From da8ab4530320388cfef978a034d60645e332a6bd Mon Sep 17 00:00:00 2001 From: Michel de Graaf Date: Mon, 7 Sep 2026 07:09:50 +0000 Subject: [PATCH 1/3] feat(tui): results across the top, with panes you can hide, zoom and Tab through The results pane now has the whole width, where a long file name needs it, and Searches, Downloads and Info share the row underneath. Tab and Shift-Tab walk the panes, z zooms the focused one to the window, w hides it so the others widen, and its number brings it back. Every list takes the same navigation keys, the shortcuts bar wraps instead of cutting keys off, and ? lists every key by pane, scrolling on a short terminal. Follows up on #55. --- README.md | 9 + fixtures/demo.svg | 2 +- soulseek-rs/src/models/app_state.rs | 196 ++++++++ soulseek-rs/src/models/mod.rs | 2 +- soulseek-rs/src/ui/main_tui/input.rs | 210 ++++++--- soulseek-rs/src/ui/main_tui/mod.rs | 311 +++++++++++- soulseek-rs/src/ui/main_tui/render.rs | 468 +++++++++++++++---- soulseek-rs/src/ui/main_tui/search.rs | 4 +- soulseek-rs/src/ui/panes/downloads_pane.rs | 11 +- soulseek-rs/src/ui/panes/searches_pane.rs | 10 +- soulseek-rs/src/ui/styles.rs | 101 +++- website/src/components/tui/data.ts | 7 +- website/src/components/tui/hero-terminal.tsx | 67 ++- website/src/components/tui/panes.tsx | 2 +- website/src/pages/docs.tsx | 41 +- 15 files changed, 1209 insertions(+), 232 deletions(-) diff --git a/README.md b/README.md index 0621669..df879aa 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,15 @@ soulseek-rs Everything else is a one-shot command that runs headless. +### The TUI + +Results run across the top, with the whole width for long file names; +Searches, Downloads and Info share the row underneath. `Tab`/`Shift-Tab` or +`1`–`3` move the focus between panes, `z` zooms the focused pane to the whole +window, and `w` hides it so the others widen (its number brings it back). +Every list takes `↑`/`↓`, `Home`/`End`, `PgUp`/`PgDn` and the vim keys, and +`?` shows every key for where you are. + ### Daemon mode A one-shot command logs in, does its job, and exits. Run a daemon instead and diff --git a/fixtures/demo.svg b/fixtures/demo.svg index 00c714c..0f07359 100644 --- a/fixtures/demo.svg +++ b/fixtures/demo.svg @@ -1 +1 @@ -╭soulseek-rslogin───────────────────────────────╮│Username:jackass44││Password:•••••••••││││Connecting…││Newusernamesareregisteredautomatically.││Tab:switch·Enter:login·Esc:quit│╰──────────────────────────────────────────────────╯╭Status────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│soulseek-rs🦀v7.0.0Downloads:0active,0completed,0fail│╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╭[1]Searches───────────────────────╮╭[2]Results────────────────────────────────────────────────────────────────────────────╮│StatusQueryResults││soulseek-rsv7.0.0││›Done tiesto 0││││Donethexx0││Noresultsyet.││Donedjbount0││Pickasearchin[1]Searches,orstartanewone.│││││││╰─────────────────────────────────────────────────────────────────────────────────────────╯││╭[3]Downloads/Uploads──────────────────────────────╮╭[Info]──────────────────────────╮│││StatUserProgressSpeed││RavingGeorgefeat.Oscar&The││││›⋯ Dancemaniako Queued -││Wolf-TwoClocks(MaceoPlex││││⋯DancemaniakoQueued-││Remix).mp3││││⋯DancemaniakoQueued-││@@kpmeh/-2013-/Electro,Deep,││││││TechnoHouse&TranceCollection││││││byLORDEGRAFvol.12││││││││││││UserDancemaniako││││││Size9.4MB││││││StatusQueued│╰─────────────────────────────────────╯╰─────────────────────────────────────────────────────╯╰──────────────────────────────────╯╭Shortcuts·Sharing:2folders────────────────────────────────────────────────────────────────────────────────────────────────╮│[s→search][m→message][i→inbox][c→chat][b→browseuser][1-3→focuspane][↑↓→navigate][Enter→results⟨✉1⟩││[s→search][m→message][i→inbox][c→chat][b→browseuser][1-3→focuspane][↑↓→navigate][Enter→results⟨✉1⟩│╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│search:│╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│[Type→searchterm][←→→movecursor][Backspace/Del→edit][Enter→search][Esc→cancel]⟨✉1⟩││[Type→searchterm][←→→movecursor][Backspace/Del→edit][Enter→search][Esc→cancel]⟨✉1⟩│╭[1]Searches───────────────────────╮╭[2]Results:SaveYourAtoll───────────────────────────────────────────────────────────╮│StatusQueryResults││soulseek-rsv7.0.0││Donetiesto0││││Donethexx0││Noresultsyet.││Donedjbount0││Pickasearchin[1]Searches,orstartanewone.││›Active Save Yo Searching.│││││││││╰─────────────────────────────────────────────────────────────────────────────────────────╯││╭[3]Downloads/Uploads──────────────────────────────╮╭[Info]──────────────────────────╮│││StatUserProgressSpeed││RavingGeorgefeat.Oscar&The││││›⋯ Dancemaniako Queued -││Wolf-TwoClocks(MaceoPlex││││⋯DancemaniakoQueued-││Remix).mp3││││⋯DancemaniakoQueued-││@@kpmeh/-2013-/Electro,Deep,││││││TechnoHouse&TranceCollection││││││byLORDEGRAFvol.12││││││││││││UserDancemaniako││││││Size9.4MB││││││StatusQueued│╰─────────────────────────────────────╯╰─────────────────────────────────────────────────────╯╰──────────────────────────────────╯│[Space→select][Enter→download][b→browseowner][c→chat][/→filter][a/A→selectall/none][1-3→focuspan⟨✉1⟩││[Space→select][Enter→download][b→browseowner][c→chat][/→filter][a/A→selectall/none][1-3→focuspan⟨✉1⟩││StatusQueryResults││✓FilenameSizeUserBitrateSpeedSlots││Donetiesto0││›[ ] @@kpmeh\- 2021 -\The P 16.5 MB Dancemaniako 320 kbps 0.46 MB/s 1││Donethexx0││[]@@vqmcr\20241128\Save56.0MBQadavre-1.69MB/s1││Donedjbount0││[]@@vqmcr\20241128\Save70.0MBQadavre-1.69MB/s1││││[]@@vqmcr\20241128\Save80.4MBQadavre-1.69MB/s1││›Active Save Yo 219││[]@@vqmcr\20241128\Save60.7MBQadavre-1.69MB/s1││││[]@@vqmcr\20241128\Save64.3MBQadavre-1.69MB/s1││││[]@@vqmcr\20241128\Save64.6MBQadavre-1.69MB/s1││││[]@@vqmcr\20241128\Save57.9MBQadavre-1.69MB/s1││││[]@@vqmcr\20241128\Save67.2MBQadavre-1.69MB/s1││││[]@@vqmcr\20241128\Save68.8MBQadavre-1.69MB/s1││││[]@@vqmcr\20241128\Save59.1MBQadavre-1.69MB/s1││││[]@@vqmcr\20241128\Save68.0MBQadavre-1.69MB/s1││││[]@@vqmcr\20241128\Save59.7MBQadavre-1.69MB/s1│╭[1]Searches───────────────────────╮╭[2]Results:MaceoPlex────────────────────────────────────────────────────────────────╮│ActiveSaveYo219││││›Active Maceo P Searching.││││││││StatusInprogress││Donetiesto0││›[ ] Music Sorted\#Beatport 17.8 MB beatrice.songbi 320 kbps 1.87 MB/s 1││Donethexx0││[]Music\#DJPools\2026\218.1MBbeatrice.songbi320kbps1.87MB/s1││Donedjbount0││[]Music\0dayMP3\2026\200.0MBbeatrice.songbi-1.87MB/s1│╭[1]Searches───────────────────────╮╭[2]Results:MaceoPlex────────────────────────────────────────────────────────────────╮│StatusQueryResults││✓FilenameSizeUserBitrateSpeedSlots││Donetiesto0││›[ ] Music Sorted\#Beatport 17.8 MB beatrice.songbi 320 kbps 1.87 MB/s 1││Donethexx0││[]Music\#DJPools\2026\218.1MBbeatrice.songbi320kbps1.87MB/s1││Donedjbount0││[]Music\0dayMP3\2026\200.0MBbeatrice.songbi-1.87MB/s1││││[]Music\0dayMP3\2026\20207.7MBbeatrice.songbi320kbps1.87MB/s1││││[]@@jlksj\new_music\Ali14.0MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\Ellu16.9MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\Ellu15.1MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\Ellu16.1MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\Ellu17.5MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\Gabr23.6MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\Gabr12.9MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\Gabr18.0MBQadavre320kbps1.69MB/s1││││││StatusInprogress││›Active Save Yo 227││[]@@vqmcr\20241128\Save60.7MBQadavre-1.69MB/s1││││[]@@vqmcr\20241128\VA-29.6MBQadavre-1.69MB/s1││││[]@@vqmcr\20241128\VA-27.0MBQadavre-1.69MB/s1││ActiveMaceoP589││[]@@vqmcr\20241128\Save80.4MBQadavre-1.69MB/s1││Donetiesto0││[]@@lustt\complete\ashur14.5MBvivio320kbps1.16MB/s1││Donethexx0││[]@@lustt\complete\cyber15.7MBvivio320kbps1.16MB/s1││Donedjbount0││[]@@lustt\complete\cyber14.7MBvivio320kbps1.16MB/s1││›Active Save Yo 227││[]@@lustt\complete\cyber15.7MBvivio320kbps1.16MB/s1││ActiveMaceoP589││[]@@lustt\complete\cyber14.4MBvivio320kbps1.16MB/s1││││[]@@lustt\complete\cyber16.3MBvivio320kbps1.16MB/s1││││[]@@lustt\complete\koala13.4MBvivio320kbps1.16MB/s1││││[]@@lustt\complete\koala13.3MBvivio320kbps1.16MB/s1││││[]@@lustt\complete\koala18.3MBvivio320kbps1.16MB/s1││││[]@@lustt\complete\koala15.8MBvivio320kbps1.16MB/s1││││[]@@lustt\complete\koala15.7MBvivio320kbps1.16MB/s1││││[]@@lustt\complete\koala14.7MBvivio320kbps1.16MB/s1││││[]@@lustt\complete\koala13.0MBvivio320kbps1.16MB/s1││││[]@@lustt\complete\panin16.7MBvivio320kbps1.16MB/s1││Donetiesto0││[]@@qxibn\music2024\Sav80.4MBsillysillysilly-0.64MB/s0││Donethexx0││[]@@qxibn\music2024\Sav64.3MBsillysillysilly-0.64MB/s0││Donedjbount0││[]@@qxibn\music2024\Sav64.6MBsillysillysilly-0.64MB/s0││││[]@@qxibn\music2024\Sav68.8MBsillysillysilly-0.64MB/s0││││[]@@qxibn\music2024\Sav59.1MBsillysillysilly-0.64MB/s0││││[]@@qxibn\music2024\Sav68.0MBsillysillysilly-0.64MB/s0││││[]@@qxibn\music2024\Sav59.7MBsillysillysilly-0.64MB/s0││││[]@@qxibn\music2024\Sav1.9MBsillysillysilly-0.64MB/s0││││[]@@wztnu\MUZYKA\FLAC\TE26.9MBDozel-0.02MB/s1││││[]@@puclh\Marzo2024\05-60.8MBneska1411kbps1.77MB/s1││││[]Music\0dayFLAC\2026\20.4MBbeatrice.songbi-1.87MB/s1││›Done Save Yo 229││[]@@qxibn\music2024\Sav57.9MBsillysillysilly-0.64MB/s0││ActiveMaceoP612││[]@@qxibn\music2024\Sav67.2MBsillysillysilly-0.64MB/s0││││›[ ] @@wztnu\MUZYKA\FLAC\TE 26.9 MB Dozel - 0.02 MB/s 1││││[✓]@@qxibn\music2024\Sav59.7MBsillysillysilly-0.64MB/s0││││›[✓] @@wztnu\MUZYKA\FLAC\TE 26.9 MB Dozel - 0.02 MB/s 1││DoneMaceoP612││[]@@qxibn\music2024\Sav67.2MBsillysillysilly-0.64MB/s0││││⋯DozelQueued-││TechnoHouse&TranceCollection││││⋯QadavreQueued-││byLORDEGRAFvol.12││││⋯vivioQueued-││││││⋯sillysillysillyQueued-││UserDancemaniako││││↯Dozel0.0MB/26.9MB0.3MB/s││TechnoHouse&TranceCollection│╭[1]Searches───────────────────────╮╭[2]Results:SaveYourAtoll───────────────────────────────────────────────────────────╮│Donetiesto0││[]@@qxibn\music2024\Sav80.4MBsillysillysilly-0.64MB/s0││Donethexx0││[]@@qxibn\music2024\Sav64.3MBsillysillysilly-0.64MB/s0││Donedjbount0││[]@@qxibn\music2024\Sav64.6MBsillysillysilly-0.64MB/s0││││[]@@qxibn\music2024\Sav68.8MBsillysillysilly-0.64MB/s0││││[]@@qxibn\music2024\Sav59.1MBsillysillysilly-0.64MB/s0││││[]@@qxibn\music2024\Sav68.0MBsillysillysilly-0.64MB/s0││││[]@@qxibn\music2024\Sav59.7MBsillysillysilly-0.64MB/s0││││[]@@qxibn\music2024\Sav1.9MBsillysillysilly-0.64MB/s0││││[]@@wztnu\MUZYKA\FLAC\TE26.9MBDozel-0.02MB/s1││││›[ ] @@wztnu\MUZYKA\FLAC\TE 26.9 MB Dozel - 0.02 MB/s 1││││[]@@puclh\Marzo2024\05-60.8MBneska1411kbps1.77MB/s1││││[]Music\0dayFLAC\2026\20.4MBbeatrice.songbi-1.87MB/s1││││⋯sillysillysillyQueued-││UserDancemaniako││DoneSaveYo229││[]Music\0dayMP3\2026\200.0MBbeatrice.songbi-1.87MB/s1││›Done Maceo P 612││[]Music\0dayMP3\2026\200.0MBbeatrice.songbi-1.87MB/s1││││[]Music\0dayMP3\2026\20207.7MBbeatrice.songbi320kbps1.87MB/s1││││[]@@jlksj\new_music\Ali14.0MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\Ellu16.9MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\Ellu15.1MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\Ellu16.1MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\Ellu17.5MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\Gabr23.6MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\Gabr12.9MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\Gabr18.0MBQadavre320kbps1.69MB/s1││Donetiesto0││[]MusicSorted\#Beatport17.8MBbeatrice.songbi320kbps1.87MB/s1││││↯Dozel0.2MB/26.9MB0.4MB/s││TechnoHouse&TranceCollection││││✓vivioCompleted-││││Donethexx0││[✓]Music\#DJPools\2026\218.1MBbeatrice.songbi320kbps1.87MB/s1││Donedjbount0││[✓]Music\0dayMP3\2026\200.0MBbeatrice.songbi-1.87MB/s1││││›[ ] Music\0day MP3\2026\20 207.7 MB beatrice.songbi 320 kbps 1.87 MB/s 1││││›✓ Dancemaniako Completed -││Wolf-TwoClocks(MaceoPlex││││││StatusCompleted││││↯Dozel0.2MB/26.9MB0.2MB/s││TechnoHouse&TranceCollection││││↯Dancemaniako0.0MB/11.1MB0.0MB/s││Remix).mp3││││[✓]@@jlksj\new_music\Ellu17.5MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\july16.0MBQadavre320kbps1.69MB/s1││││↯Dozel0.3MB/26.9MB0.5MB/s││TechnoHouse&TranceCollection││Donetiesto0││[]@@kpmeh\-2013-\2013.10.4MBDancemaniako192kbps0.46MB/s1││Donethexx0││[]@@kpmeh\-2013-\2013.7.3MBDancemaniako192kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\-2013-\AllG40.0MBDancemaniako-0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2013-\Azari50.9MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2013-\Beatp18.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\Beatp17.4MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\Butch57.5MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2013-\Deep15.6MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\Defec19.0MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\Defec17.2MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\Defec18.4MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\Defec54.6MBDancemaniako-0.46MB/s1││││›[ ] @@kpmeh\- 2013 -\E-Dan 11.1 MB Dancemaniako 320 kbps 0.46 MB/s 1││DoneSaveYo245││[]@@kpmeh\-2013-\Armad17.5MBDancemaniako320kbps0.46MB/s1││││[✓]@@kpmeh\-2013-\E-Dan11.1MBDancemaniako320kbps0.46MB/s1││││↯Dozel0.4MB/26.9MB0.3MB/s││TechnoHouse&TranceCollection││││↯Dozel0.4MB/26.9MB0.1MB/s││TechnoHouse&TranceCollection││││[]@@kpmeh\-2014-\Armad15.3MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\Armad10.5MBDancemaniako320kbps0.46MB/s1││││↯Dozel0.5MB/26.9MB0.4MB/s││TechnoHouse&TranceCollection││││[]@@kpmeh\-2015-\VA-16.9MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2016-\Top114.7MBDancemaniako320kbps0.46MB/s1││││↯Dozel0.5MB/26.9MB0.3MB/s││TechnoHouse&TranceCollection││││[]@@kpmeh\-2017-\Beatp16.5MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2017-\Beatp16.0MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2017-\Beatp14.9MBDancemaniako320kbps0.46MB/s1││││↯Dozel0.6MB/26.9MB0.3MB/s││TechnoHouse&TranceCollection││││[]@@kpmeh\-2022-\Defec19.0MBDancemaniako320kbps0.46MB/s1││DoneSaveYo245││[]Music\0dayMP3\2026\200.0MBbeatrice.songbi-1.87MB/s1││││[✓]@@jlksj\new_music\Gabr23.6MBQadavre320kbps1.69MB/s1││││↯Dozel0.7MB/26.9MB0.4MB/s││TechnoHouse&TranceCollection││││✓QadavreCompleted-││byLORDEGRAFvol.12││Donetiesto0││[]music3\MaceoPlex-DJ35.2MBcinstitute2-4.4MB/s1││Donethexx0││[]music3\MaceoPlex-DJ35.9MBcinstitute2-4.4MB/s1││Donedjbount0││[]music3\MaceoPlex-DJ27.7MBcinstitute2-4.4MB/s1││DoneSaveYo245││[]music3\MaceoPlex-DJ37.7MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ30.0MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ24.5MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ43.8MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ26.3MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ26.8MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ40.8MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ0.0MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ0.2MBcinstitute2-4.4MB/s1││›Done Maceo P 612││[✓]music3\MaceoPlex-DJ26.2MBcinstitute2-4.4MB/s1││││↯Dozel0.8MB/26.9MB0.1MB/s││TechnoHouse&TranceCollection││││↯Dozel0.8MB/26.9MB0.4MB/s││TechnoHouse&TranceCollection││›Done Maceo P 612││[]music3\MaceoPlex-DJ26.2MBcinstitute2-4.4MB/s1││││›[ ] music3\Maceo Plex - DJ 26.3 MB cinstitute2 - 4.4 MB/s 1││││⋯cinstitute2Queued-││Size9.4MB││││⋯cinstitute2Queued-││StatusCompleted│╭[1]Searches───────────────────────╮╭[2]Results:MaceoPlex────────────────────────────────────────────────────────────────╮│StatusQueryResults││✓FilenameSizeUserBitrateSpeedSlots││Donetiesto0││[]music3\MaceoPlex-DJ35.2MBcinstitute2-4.4MB/s1││Donethexx0││[]music3\MaceoPlex-DJ35.9MBcinstitute2-4.4MB/s1││Donedjbount0││[]music3\MaceoPlex-DJ27.7MBcinstitute2-4.4MB/s1││DoneSaveYo245││[]music3\MaceoPlex-DJ37.7MBcinstitute2-4.4MB/s1││›Done Maceo P 612││[]music3\MaceoPlex-DJ26.2MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ30.0MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ24.5MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ43.8MBcinstitute2-4.4MB/s1││││›[ ] music3\Maceo Plex - DJ 26.3 MB cinstitute2 - 4.4 MB/s 1││││[]music3\MaceoPlex-DJ26.8MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ40.8MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ0.0MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ0.2MBcinstitute2-4.4MB/s1│││╰─────────────────────────────────────────────────────────────────────────────────────────╯││╭[3]Downloads/Uploads──────────────────────────────╮╭[Info]──────────────────────────╮│││StatUserProgressSpeed││RavingGeorgefeat.Oscar&The││││›✓ Dancemaniako Completed -││Wolf-TwoClocks(MaceoPlex││││⋯DancemaniakoQueued-││@@kpmeh/-2013-/Electro,Deep,││││↯Dozel0.9MB/26.9MB0.3MB/s││TechnoHouse&TranceCollection││││✓QadavreCompleted-││byLORDEGRAFvol.12││││✓vivioCompleted-││││││⋯sillysillysillyQueued-││UserDancemaniako││││⋯cinstitute2Queued-││StatusCompleted│╰─────────────────────────────────────╯╰─────────────────────────────────────────────────────╯╰──────────────────────────────────╯│[p→pause/resume][r→retryfailed][d→deletequeued][c→clearfinished][b→browseuser][1-3→focuspane][q⟨✉1⟩││[p→pause/resume][r→retryfailed][d→deletequeued][c→clearfinished][b→browseuser][1-3→focuspane][q⟨✉1⟩││││⋯cinstitute2Queued-││Saveto││││StatUserProgressSpeed││020.MaceoPlex-Insomnia││││⋯sillysillysillyQueued-││2022.mp3││││⋯cinstitute2Queued-││ClubbingElectroHouseVol.12││││✓beatrice.songbiCompleted-││││││›⋯ Dancemaniako Queued -││/Users/micheldegraaf/Downloads/S││[p→pause/resume][r→retryfailed][d→deletequeued][c→clearfinished][b→browseuser][1-3→focuspane][q⟨✉2⟩││││⋯cinstitute2Queued-││││││✓beatrice.songbiCompleted-││UserDancemaniako││││⋯DancemaniakoQueued-││Saveto││││⋯DancemaniakoQueued-││/Users/micheldegraaf/Downloads/S││[p→pause/resume][r→retryfailed][d→deletequeued][c→clearfinished][b→browseuser][1-3→focuspane][q⟨✉2⟩││││StatUserProgressSpeed││OddParents,MaceoPlex-Learn││││✓cinstitute2Completed-││ToFly(Maceo's808Dub).mp3││││✓beatrice.songbiCompleted-││#BeatportdecadeTechHouse││││✓beatrice.songbiCompleted-││UserQadavre││││›✓ Qadavre Completed -││Size17.5MB││││StatUserProgressSpeed││OddParents,MaceoPlex-Learn││││✓cinstitute2Completed-││ToFly(Maceo's808Dub).mp3││││✓beatrice.songbiCompleted-││#BeatportdecadeTechHouse││││⋯cinstitute2Queued-││││││✓beatrice.songbiCompleted-││UserQadavre││││›✓ Qadavre Completed -││Size17.5MB││││⋯DancemaniakoQueued-││Saveto││││⋯DancemaniakoQueued-││/Users/micheldegraaf/Downloads/S││[Space→select][Enter→download][b→browseowner][c→chat][/→filter][a/A→selectall/none][1-3→focuspan⟨✉2⟩││[Space→select][Enter→download][b→browseowner][c→chat][/→filter][a/A→selectall/none][1-3→focuspan⟨✉2⟩││││✓cinstitute2Completed-││@@jlksj/new_music/EllumAudio││Status╭Rooms(Enter:join,/:filter,Tab:openrooms,Esc:close)───────────────────────────────────────────────╮Slots││Done│roomusers│1││Done│› ! ! LGBTQ+ ! ! 461│1││Done│RareMusic320│1││Done│Linux262│1││›Done│SoulSync229│1│││lossless199│1│││indie180│1│││+BlackMetal+153│1│││drum'n'bass150│1│││/mu/148│1│││what.cd138│1│││Furry135│1│││:3126│1│││TECHNO,MixesandTunes122│1│││r/musichoarder119│1│││japanesemusic113│─────────╯││MOVIES105│─────────╮││Ambient89│-Learn│││Anime79│.mp3│││!!!cumeaters!!!77│Audio│││EBooks77│se│││HOUSEMUSICLOVERS(AG)69││││+Autism+62│vre│││Hardcore/punk62│MB│││!meowchat:361│leted│││programming61│││╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯nloads/S││[↑↓→move][Enter→join][/→filter][Tab→openrooms][Esc→close]⟨✉2⟩││[↑↓→move][Enter→join][/→filter][Tab→openrooms][Esc→close]⟨✉2⟩││Done│nicotine1270│1││Done│!!LGBTQ+!!466│1││Done│› Rare Music 327│1││›Done│Linux264│1│││SoulSync230│1│││TheLobby215│1│││lossless200│1│││indie181│1│││+BlackMetal+155│1│││/mu/151│1│││drum'n'bass150│1│││what.cd139│1│││Furry137│1│││TECHNO,MixesandTunes122│─────────╯││r/musichoarder120│─────────╮││japanesemusic117│-Learn│││MOVIES106│.mp3│││Ambient89│Audio│││Anime79│se│││EBooks77││││!!!cumeaters!!!76│vre│││HOUSEMUSICLOVERS(AG)68│MB│││!meowchat:362│leted│││+Autism+62│││Status╭Chatrooms(Tab:switch,l:roomlist,x:leave,Esc:back)───────────────────────────────────────────────╮Slots││Done│Rare Music│1││Done│04:59→jackass44│Users(1)│1││Done││▸ jackass44│1││Done│││1││›Done│││1│││││1│││││1│││││─────────╯││││─────────╮││││-Learn│││││.mp3│││││Audio│││││se││││││││││vre│││││MB│││││leted│││Enter:typeamessage│││[Enter→say][↑↓→pickuser][b→browseuser][m→messageuser][Tab→switchroom][l→rooms][x→leave]⟨✉2⟩││[Enter→say][↑↓→pickuser][b→browseuser][m→messageuser][Tab→switchroom][l→rooms][x→leave]⟨✉2⟩││[Type→message][Enter→send][Esc→cancel]⟨✉2⟩││Done│04:59→jackass44│Users(328)│1││Done│04:59—joined—│▸ mayonerio│1││Done││bradybunch668│1││›Done││splashdown│1││││1800thrownaway│1││││baza333│1││││Galaxion│1││││enricopoliman│1││││smilerrr│1││││DJ_Hannibalistic│1││││toddomg│1││││niet│1││││kebnimqojom│1││││beautysleep│1││││sSsSs│─────────╯│││ratgirrl│─────────╮│││v3rtual│-Learn││││professionalspeed│.mp3││││kasmanga1│Audio││││uvizj4yt8f75│se││││SBORRANET123│││││xeroxeom│vre││││osofer│MB││││craigp56│leted││[Type→message][Enter→send][Esc→cancel]⟨✉2⟩│││›helloworld▏│││Done│04:59←stupidmusichoarder│bradybunch668│1││Done│04:59→jackass44│Users(326)│1││›Done│04:59←Bipolar213│splashdown│1│││04:59<jackass44>helloworld!│1800thrownaway│1││Done│04:59→jackass44│Users(325)│1│││04:59←dudebro│baza333│1│││04:59→martitoci│Galaxion│1││Done│04:59—joined—│mayonerio│1││›Done│04:59←Bipolar213│▸ splashdown│1││Status╭Browsesplashdown───────────────────────────────────────────────────────────────────────────────────╮Slots││Done│⠋Requestingsharedfilesfromsplashdown…│1││Done││1││›Done││1││││1││││1││││────────────╯│││────────────╮│││ex-Learn││││ub).mp3││││umAudio││││House││││││││adavre││││7.5MB││││ompleted││╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯Downloads/S││[↑↓→move][→←→expand/collapse][Enter→open/download][d→downloadfolder][Tab→switchuser][r→retry][w→c⟨✉2⟩││Done│⠴Requestingsharedfilesfromsplashdown…│1││[↑↓→move][→←→expand/collapse][Enter→open/download][d→downloadfolder][Tab→switchuser][r→retry][w→c⟨✉2⟩││││✓cinstitute2Completed-││││││✓QadavreCompleted-││StatusCompleted││Doned╭Settings───────────────────────────────────────────────────────────────────────────────╮4MB/s1││DoneS│>Downloadfolder:/Users/micheldegraaf/Downloads/Soulseek│4MB/s1││›Done M││4MB/s1│││Sharedfolders(2):│4MB/s1│││/Users/micheldegraaf/Downloads/Soulseek│4MB/s1│││/Users/micheldegraaf/Downloads│4MB/s1││││4 MB/s 1││││4MB/s1││││──────────────────╯│││──────────────────╮│││ceoPlex-Learn││││808Dub).mp3││││ic/EllumAudio││││TechHouse││││││││Qadavre││╰─────────────────────────────────────────────────────────────────────────────────────────╯17.5MB││[↑↓→move][Enter/e→editdownloaddir][a→addshare][d→removeshare][r→re-index][Esc→close]⟨✉2⟩││[↑↓→move][Enter/e→editdownloaddir][a→addshare][d→removeshare][r→re-index][Esc→close]⟨✉2⟩││search:SaveYourAtoll││›Active Save Yo 85││[]@@vqmcr\20241128\Save60.7MBQadavre-1.69MB/s1││││[]@@vqmcr\2││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail0.0/9.4MB·0MB/s·[░░░░░░░░░░░░░░░░░░░░░]0%││││›↯ Dancemaniako 0.0 MB/9.4 MB ( 0.0 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail0.1/9.4MB·0.11MB/s·[░░░░░░░░░░░░░░░░░░░░░]0%││││›↯ Dancemaniako 0.1 MB/9.4 MB ( 0.1 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail0.4/9.4MB·0.56MB/s·[░░░░░░░░░░░░░░░░░░░░░]3%││││›↯ Dancemaniako 0.4 MB/9.4 MB ( 0.6 MB/s││Wolf-TwoClocks(MaceoPlex││search:MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail0.6/9.4MB·0.61MB/s·[█░░░░░░░░░░░░░░░░░░░░]6%││││›↯ Dancemaniako 0.6 MB/9.4 MB ( 0.6 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail0.9/9.4MB·0.62MB/s·[█░░░░░░░░░░░░░░░░░░░░]9%││││›↯ Dancemaniako 0.9 MB/9.4 MB ( 0.6 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail1.1/9.4MB·0.48MB/s·[██░░░░░░░░░░░░░░░░░░░]11%││ActiveSaveYo219││[]Music\0dayMP3\2026\20││││›↯ Dancemaniako 1.0 MB/9.4 MB ( 0.8 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail1.3/9.4MB·0.47MB/s·[██░░░░░░░░░░░░░░░░░░░]14%││ActiveSaveYo219││[]Music\0dayMP3\2026\200.0MBbeatrice.songbi-1.87MB/s1││›Active Maceo P 56││[]Music\0dayMP3\2026\200.0MBbeatrice.songbi-1.87MB/s1││││›↯ Dancemaniako 1.3 MB/9.4 MB ( 0.5 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail1.7/9.4MB·1.6MB/s·[███░░░░░░░░░░░░░░░░░░]17%││›Active Save Yo 225││[]Music\0dayMP3\2026\200.0MBbeatrice.songbi-1.87MB/s1││ActiveMaceoP56││[]Music\0dayMP3\2026\200.0MBbeatrice.songbi-1.87MB/s1││││›↯ Dancemaniako 1.7 MB/9.4 MB ( 1.6 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail1.9/9.4MB·0.46MB/s·[████░░░░░░░░░░░░░░░░░]19%││ActiveMaceoP56││[]@@vqmcr\20241128\Save80.4MBQadavre-1.69MB/s1││││›↯ Dancemaniako 1.9 MB/9.4 MB ( 0.5 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail2.1/9.4MB·0.48MB/s·[████░░░░░░░░░░░░░░░░░]21%││││›↯ Dancemaniako 2.1 MB/9.4 MB ( 0.5 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail2.3/9.4MB·1.26MB/s·[█████░░░░░░░░░░░░░░░░]24%││Donetiesto0││[]@@kpmeh\-2021-\TheP16.5MBDancemaniako320kbps0.46MB/s1││Donedjbount0││›[✓] @@vqmcr\20241128\Save 70.0 MB Qadavre - 1.69 MB/s 1││││›↯ Dancemaniako 2.3 MB/9.4 MB ( 1.3 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail2.6/9.4MB·0.49MB/s·[█████░░░░░░░░░░░░░░░░]27%││Donetiesto0││[]@@vqmcr\20241128\Save56.0MBQadavre-1.69MB/s1││Donethexx0││[✓]@@vqmcr\20241128\Save70.0MBQadavre-1.69MB/s1││Donedjbount0││[]@@vqmcr\20241128\Save60.7MBQadavre-1.69MB/s1││›Active Save Yo 227││[]@@vqmcr\20241128\Save80.4MBQadavre-1.69MB/s1││ActiveMaceoP589││[]@@vqmcr\20241128\Save64.3MBQadavre-1.69MB/s1││││›[ ] @@vqmcr\20241128\VA-Pr 37.0 MB Qadavre - 1.69 MB/s 1││││›↯ Dancemaniako 2.6 MB/9.4 MB ( 0.5 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail2.9/9.4MB·1.24MB/s·[██████░░░░░░░░░░░░░░░]30%││││[]@@lustt\complete\koala13.6MBvivio320kbps1.16MB/s1││││›[ ] @@lustt\complete\koala 14.4 MB vivio 320 kbps 1.16 MB/s 1││││›↯ Dancemaniako 2.9 MB/9.4 MB ( 1.2 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail3.1/9.4MB·1.13MB/s·[██████░░░░░░░░░░░░░░░]32%││││›[✓] @@lustt\complete\koala 13.6 MB vivio 320 kbps 1.16 MB/s 1││││[]@@lustt\complete\koala14.4MBvivio320kbps1.16MB/s1││││›↯ Dancemaniako 3.1 MB/9.4 MB ( 1.1 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail3.3/9.4MB·0.44MB/s·[███████░░░░░░░░░░░░░░]35%││Donetiesto0││[]@@lustt\complete\koala14.4MBvivio320kbps1.16MB/s1││Donethexx0││[]@@lustt\complete\koala16.3MBvivio320kbps1.16MB/s1││Donedjbount0││[]@@lustt\complete\koala14.7MBvivio320kbps1.16MB/s1││›Done Save Yo 227││[]@@lustt\complete\koala14.8MBvivio320kbps1.16MB/s1││ActiveMaceoP589││[]@@lustt\complete\koala16.5MBvivio320kbps1.16MB/s1││││[]@@lustt\complete\koala15.6MBvivio320kbps1.16MB/s1││││[]@@lustt\complete\koala15.3MBvivio320kbps1.16MB/s1││││[]@@lustt\complete\liuzi15.2MBvivio-1.16MB/s1││││[]@@lustt\complete\liuzi16.3MBvivio-1.16MB/s1││││[]@@lustt\complete\panin16.8MBvivio320kbps1.16MB/s1││││[]@@lustt\complete\panin19.9MBvivio320kbps1.16MB/s1││││›[ ] @@lustt\complete\panin 17.9 MB vivio 320 kbps 1.16 MB/s 1││││›↯ Dancemaniako 3.3 MB/9.4 MB ( 0.4 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail3.6/9.4MB·0.82MB/s·[███████░░░░░░░░░░░░░░]37%││Donetiesto0││[]@@lustt\complete\panin16.2MBvivio320kbps1.16MB/s1││Donethexx0││[]@@lustt\complete\panin15.4MBvivio320kbps1.16MB/s1││Donedjbount0││[]@@lustt\complete\panin19.4MBvivio320kbps1.16MB/s1││›Done Save Yo 227││[]@@lustt\complete\panin19.4MBvivio320kbps1.16MB/s1││ActiveMaceoP589││[]@@lustt\complete\panin14.3MBvivio320kbps1.16MB/s1││││[]@@lustt\complete\panin19.0MBvivio320kbps1.16MB/s1││││[]@@lustt\complete\panin15.8MBvivio320kbps1.16MB/s1││││[]@@lustt\complete\panin16.9MBvivio320kbps1.16MB/s1││││[]@@lustt\complete\panin15.2MBvivio320kbps1.16MB/s1││││[]@@qxibn\music2024\Osl64.9MBsillysillysilly-0.64MB/s0││││[]@@qxibn\music2024\Sav56.0MBsillysillysilly-0.64MB/s0││││[]@@qxibn\music2024\Sav70.0MBsillysillysilly-0.64MB/s0││││[]@@qxibn\music2024\Sav60.7MBsillysillysilly-0.64MB/s0││││›[ ] @@qxibn\music 2024\Sav 80.4 MB sillysillysilly - 0.64 MB/s 0││││›↯ Dancemaniako 3.6 MB/9.4 MB ( 0.8 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail3.7/9.4MB·0.5MB/s·[████████░░░░░░░░░░░░░]39%││›Done Save Yo 227││[]@@qxibn\music2024\Sav57.9MBsillysillysilly-0.64MB/s0││ActiveMaceoP589││[]@@qxibn\music2024\Sav67.2MBsillysillysilly-0.64MB/s0││││›[ ] @@puclh\Marzo 2024\05- 60.8 MB neska 1411 kbps 1.77 MB/s 1││││›↯ Dancemaniako 3.7 MB/9.4 MB ( 0.5 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail4.1/9.4MB·0.56MB/s·[█████████░░░░░░░░░░░░]43%││││›↯ Dancemaniako 4.1 MB/9.4 MB ( 0.6 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail4.3/9.4MB·2.07MB/s·[█████████░░░░░░░░░░░░]45%││││›[✓] @@qxibn\music 2024\Sav 68.0 MB sillysillysilly - 0.64 MB/s 0││││›↯ Dancemaniako 4.3 MB/9.4 MB ( 2.1 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail4.6/9.4MB·0.52MB/s·[██████████░░░░░░░░░░░]49%││││›[ ] @@qxibn\music 2024\Sav 59.7 MB sillysillysilly - 0.64 MB/s 0││││›↯ Dancemaniako 4.6 MB/9.4 MB ( 0.5 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail4.9/9.4MB·3.49MB/s·[██████████░░░░░░░░░░░]52%││││›[ ] @@qxibn\music 2024\Sav 1.9 MB sillysillysilly - 0.64 MB/s 0││││›↯ Dancemaniako 4.9 MB/9.4 MB ( 3.5 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail5.2/9.4MB·2.3MB/s·[███████████░░░░░░░░░░]54%││││›↯ Dancemaniako 5.2 MB/9.4 MB ( 2.3 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail5.4/9.4MB·1.79MB/s·[████████████░░░░░░░░░]57%││││›↯ Dancemaniako 5.4 MB/9.4 MB ( 1.8 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail5.6/9.4MB·0.45MB/s·[████████████░░░░░░░░░]59%││││›↯ Dancemaniako 5.6 MB/9.4 MB ( 0.4 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail5.9/9.4MB·1.69MB/s·[█████████████░░░░░░░░]62%││││›↯ Dancemaniako 5.9 MB/9.4 MB ( 1.7 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail6.1/9.4MB·0.48MB/s·[█████████████░░░░░░░░]64%││││›↯ Dancemaniako 6.1 MB/9.4 MB ( 0.5 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:1active,0completed,0fail6.4/9.4MB·0.54MB/s·[██████████████░░░░░░░]67%││││›↯ Dancemaniako 6.4 MB/9.4 MB ( 0.5 MB/s││Wolf-TwoClocks(MaceoPlex││soulseek-rs🦀v7.0.0Downloads:4active,0completed,0fail8.3/120.1MB·42.32MB/s·[█░░░░░░░░░░░░░░░░░░░░]6%││││›↯ Dancemaniako 6.6 MB/9.4 MB ( 0.6 MB/s││Wolf-TwoClocks(MaceoPlex││││↯Qadavre1.0MB/70.0MB5.1MB/s││byLORDEGRAFvol.12││││↯vivio0.6MB/13.6MB36.3MB/s││││soulseek-rs🦀v7.0.0Downloads:4active,0completed,0fail11.5/120.1MB·10.84MB/s·[██░░░░░░░░░░░░░░░░░░░]9%││││›↯ Dancemaniako 6.8 MB/9.4 MB ( 0.5 MB/s││Wolf-TwoClocks(MaceoPlex││││↯Qadavre2.6MB/70.0MB6.7MB/s││byLORDEGRAFvol.12││││↯vivio2.1MB/13.6MB3.3MB/s││││soulseek-rs🦀v7.0.0Downloads:4active,0completed,0fail14.7/120.1MB·9.05MB/s·[██░░░░░░░░░░░░░░░░░░░]12%││││›↯ Dancemaniako 7.1 MB/9.4 MB ( 0.5 MB/s││Wolf-TwoClocks(MaceoPlex││││↯Qadavre4.2MB/70.0MB5.1MB/s││byLORDEGRAFvol.12││││↯vivio3.4MB/13.6MB3.2MB/s││││soulseek-rs🦀v7.0.0Downloads:4active,0completed,0fail18.7/120.1MB·10.55MB/s·[███░░░░░░░░░░░░░░░░░░]15%││││›↯ Dancemaniako 7.4 MB/9.4 MB ( 2.8 MB/s││Wolf-TwoClocks(MaceoPlex││││↯Qadavre6.0MB/70.0MB4.5MB/s││byLORDEGRAFvol.12││││↯vivio5.2MB/13.6MB3.0MB/s││││soulseek-rs🦀v7.0.0Downloads:4active,0completed,0fail21.9/120.1MB·11.36MB/s·[███░░░░░░░░░░░░░░░░░░]18%││││›↯ Dancemaniako 7.7 MB/9.4 MB ( 2.5 MB/s││Wolf-TwoClocks(MaceoPlex││││↯Qadavre7.6MB/70.0MB5.0MB/s││byLORDEGRAFvol.12││││↯vivio6.7MB/13.6MB3.6MB/s││││soulseek-rs🦀v7.0.0Downloads:4active,0completed,0fail25.1/120.1MB·10.33MB/s·[████░░░░░░░░░░░░░░░░░]20%││││›↯ Dancemaniako 7.9 MB/9.4 MB ( 2.1 MB/s││Wolf-TwoClocks(MaceoPlex││││↯Qadavre9.1MB/70.0MB4.9MB/s││byLORDEGRAFvol.12││││↯vivio8.0MB/13.6MB3.0MB/s││││soulseek-rs🦀v7.0.0Downloads:4active,0completed,0fail28.6/120.1MB·11.11MB/s·[█████░░░░░░░░░░░░░░░░]23%││›Done Save Yo 229││[]@@qxibn\music2024\Sav57.9MBsillysillysilly-0.64MB/s0││DoneMaceoP612││[]@@qxibn\music2024\Sav67.2MBsillysillysilly-0.64MB/s0││││›↯ Dancemaniako 8.2 MB/9.4 MB ( 1.7 MB/s││Wolf-TwoClocks(MaceoPlex││││↯Dozel0.0MB/26.9MB0.3MB/s││TechnoHouse&TranceCollection││││↯Qadavre10.8MB/70.0MB6.0MB/s││byLORDEGRAFvol.12││││↯vivio9.6MB/13.6MB3.1MB/s││││soulseek-rs🦀v7.0.0Downloads:4active,0completed,0fail32.5/120.1MB·9.85MB/s·[█████░░░░░░░░░░░░░░░░]27%││DoneSaveYo229││[]@@qxibn\music2024\Sav57.9MBsillysillysilly-0.64MB/s0││›Done Maceo P 612││[]@@qxibn\music2024\Sav67.2MBsillysillysilly-0.64MB/s0││││›↯ Dancemaniako 8.4 MB/9.4 MB ( 2.0 MB/s││Wolf-TwoClocks(MaceoPlex││││↯Dozel0.1MB/26.9MB0.3MB/s││TechnoHouse&TranceCollection││││↯Qadavre12.6MB/70.0MB4.7MB/s││byLORDEGRAFvol.12││││↯vivio11.3MB/13.6MB2.8MB/s││││soulseek-rs🦀v7.0.0Downloads:4active,0completed,0fail35.8/120.1MB·13.79MB/s·[██████░░░░░░░░░░░░░░░]29%││││›↯ Dancemaniako 8.7 MB/9.4 MB ( 2.4 MB/s││Wolf-TwoClocks(MaceoPlex││││↯Dozel0.1MB/26.9MB0.5MB/s││TechnoHouse&TranceCollection││││↯Qadavre14.4MB/70.0MB5.8MB/s││byLORDEGRAFvol.12││││↯vivio12.5MB/13.6MB5.1MB/s││││soulseek-rs🦀v7.0.0Downloads:3active,1completed,0fail25.2/106.4MB·7.35MB/s·[████░░░░░░░░░░░░░░░░░]23%││Donethexx0││›[ ] Music\#DJ Pools\2026\2 18.1 MB beatrice.songbi 320 kbps 1.87 MB/s 1││││›↯ Dancemaniako 8.9 MB/9.4 MB ( 2.1 MB/s││Wolf-TwoClocks(MaceoPlex││││↯Qadavre16.1MB/70.0MB4.8MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,1completed,0fail27.1/106.4MB·1.94MB/s·[█████░░░░░░░░░░░░░░░░]25%││Donedjbount0││›[ ] Music\0day MP3\2026\20 0.0 MB beatrice.songbi - 1.87 MB/s 1││││›↯ Dancemaniako 9.2 MB/9.4 MB ( 0.5 MB/s││Wolf-TwoClocks(MaceoPlex││││↯Qadavre17.7MB/70.0MB1.1MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:2active,2completed,0fail19.8/97.0MB·3.44MB/s·[████░░░░░░░░░░░░░░░░░]20%││││↯Dozel0.2MB/26.9MB0.1MB/s││TechnoHouse&TranceCollection││││↯Qadavre19.6MB/70.0MB3.4MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:2active,2completed,0fail21.4/97.0MB·4.71MB/s·[████░░░░░░░░░░░░░░░░░]22%││││›[ ] @@jlksj\new_music\Gabr 23.6 MB Qadavre 320 kbps 1.69 MB/s 1││││↯Qadavre21.2MB/70.0MB4.5MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail22.9/108.1MB·7.48MB/s·[████░░░░░░░░░░░░░░░░░]21%││││›[✓] @@jlksj\new_music\Ellu 17.5 MB Qadavre 320 kbps 1.69 MB/s 1││││↯Qadavre22.7MB/70.0MB7.3MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail24.6/108.1MB·6.01MB/s·[████░░░░░░░░░░░░░░░░░]22%││││›[✓] @@jlksj\new_music\Gabr 23.6 MB Qadavre 320 kbps 1.69 MB/s 1││││↯Qadavre24.4MB/70.0MB5.8MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail26.6/108.1MB·7.02MB/s·[█████░░░░░░░░░░░░░░░░]24%││Donetiesto0││[]@@jlksj\new_music\Mace16.6MBQadavre320kbps1.69MB/s1││Donethexx0││[]@@jlksj\new_music\Mace17.1MBQadavre320kbps1.69MB/s1││Donedjbount0││[]@@jlksj\new_music\Mace17.3MBQadavre320kbps1.69MB/s1││DoneSaveYo229││[]@@jlksj\new_music\VA-12.9MBQadavre320kbps1.69MB/s1││›Done Maceo P 612││[]@@jlksj\new_music\WhoM15.3MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\WhoM17.9MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\augu0.1MBQadavre-1.69MB/s1││││[]@@jlksj\new_music\augu18.3MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\june17.7MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\marc16.9MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\marc15.1MBQadavre320kbps1.69MB/s1││││[]@@jlksj\new_music\marc15.2MBQadavre320kbps1.69MB/s1││││›[ ] @@jlksj\new_music\marc 17.2 MB Qadavre 320 kbps 1.69 MB/s 1││││↯Dancemaniako0.1MB/11.1MB0.1MB/s││Remix).mp3││││↯Dozel0.3MB/26.9MB0.3MB/s││TechnoHouse&TranceCollection││││↯Qadavre26.2MB/70.0MB6.6MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail28.3/108.1MB·7.29MB/s·[█████░░░░░░░░░░░░░░░░]26%││Donetiesto0││[]@@qgkbx\tmp\GabrielAn12.9MBQadavre320kbps1.69MB/s1││Donethexx0││[]@@qgkbx\tmp\MaceoPlex20.6MBQadavre320kbps1.69MB/s1││Donedjbount0││[]@@qgkbx\tmp\MaceoPlex17.3MBQadavre320kbps1.69MB/s1││DoneSaveYo229││[]@@qgkbx\tmp\MaribouSt13.5MBQadavre320kbps1.69MB/s1││›Done Maceo P 612││[]@@qgkbx\tmp\Rebolledo16.5MBQadavre-1.69MB/s1││││[]@@qgkbx\tmp\Royksopp-S18.3MBQadavre320kbps1.69MB/s1││││[]@@vqmcr\SeagatePortabl13.6MBQadavre320kbps1.69MB/s1││││[]@@vqmcr\SeagatePortabl21.1MBQadavre320kbps1.69MB/s1││││[]@@vqmcr\SeagatePortabl11.4MBQadavre320kbps1.69MB/s1││││[]@@vqmcr\SeagatePortabl8.5MBQadavre320kbps1.69MB/s1││││[]@@vqmcr\SeagatePortabl17.4MBQadavre320kbps1.69MB/s1││││[]@@vqmcr\SeagatePortabl9.3MBQadavre320kbps1.69MB/s1││││[]@@vqmcr\SeagatePortabl15.6MBQadavre320kbps1.69MB/s1││││[]@@vqmcr\SeagatePortabl16.4MBQadavre320kbps1.69MB/s1││││›[ ] @@vqmcr\SeagatePortabl 17.5 MB Qadavre 320 kbps 1.69 MB/s 1││││↯Dancemaniako0.4MB/11.1MB0.4MB/s││Remix).mp3││││↯Qadavre27.7MB/70.0MB6.3MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail30.3/108.1MB·5.16MB/s·[█████░░░░░░░░░░░░░░░░]27%││DoneSaveYo229││[]@@kpmeh\-2013-\Armad17.5MBDancemaniako320kbps0.46MB/s1││││↯Dancemaniako0.6MB/11.1MB0.5MB/s││Remix).mp3││││↯Qadavre29.3MB/70.0MB4.1MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail32.1/108.1MB·5.73MB/s·[██████░░░░░░░░░░░░░░░]29%││││↯Dancemaniako0.9MB/11.1MB0.4MB/s││Remix).mp3││││↯Qadavre30.9MB/70.0MB4.8MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail34.2/108.1MB·11.34MB/s·[██████░░░░░░░░░░░░░░░]31%││││›[ ] @@kpmeh\- 2013 -\Defec 54.6 MB Dancemaniako - 0.46 MB/s 1││││↯Dancemaniako1.2MB/11.1MB5.9MB/s││Remix).mp3││││↯Qadavre32.6MB/70.0MB5.2MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail36.0/108.1MB·6.22MB/s·[██████░░░░░░░░░░░░░░░]33%││││›[✓] @@kpmeh\- 2013 -\Defec 54.6 MB Dancemaniako - 0.46 MB/s 1││││↯Dancemaniako1.3MB/11.1MB0.5MB/s││Remix).mp3││││↯Qadavre34.3MB/70.0MB5.5MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail38.2/108.1MB·5.47MB/s·[███████░░░░░░░░░░░░░░]35%││Donetiesto0││[]@@kpmeh\-2013-\AllG40.0MBDancemaniako-0.46MB/s1││Donethexx0││[]@@kpmeh\-2013-\Armad17.5MBDancemaniako320kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\-2013-\Azari50.9MBDancemaniako-0.46MB/s1││DoneSaveYo245││[]@@kpmeh\-2013-\Beatp18.1MBDancemaniako320kbps0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2013-\Beatp17.4MBDancemaniako320kbps0.46MB/s1││││[✓]@@kpmeh\-2013-\Defec54.6MBDancemaniako-0.46MB/s1││││[✓]@@kpmeh\-2013-\E-Dec54.6MBDancemaniako-0.46MB/s1││││›[ ] @@kpmeh\- 2013 -\Elect 9.4 MB Dancemaniako 320 kbps 0.46 MB/s 1││││↯Dancemaniako1.6MB/11.1MB0.5MB/s││Remix).mp3││││↯Qadavre36.2MB/70.0MB4.7MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail40.0/108.1MB·4.45MB/s·[███████░░░░░░░░░░░░░░]36%││Donetiesto0││[]@@kpmeh\-2013-\VA-13.3MBDancemaniako320kbps0.46MB/s1││Donethexx0││[]@@kpmeh\-2013-\VA-12.2MBDancemaniako320kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\-2013-\VA-16.2MBDancemaniako257kbps0.46MB/s1││DoneSaveYo245││[]@@kpmeh\-2013-\VA-13.4MBDancemaniako320kbps0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2013-\VA-16.7MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\VA-15.6MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\VA-9.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\VA-17.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\VA-20.4MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\VA-17.5MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\VA-19.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\VA-8.7MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2013-\VA-14.1MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2013-\VA-12.9MBDancemaniako-0.46MB/s1││││›[ ] @@kpmeh\- 2013 -\VA - 7.2 MB Dancemaniako - 0.46 MB/s 1││││↯Dancemaniako1.9MB/11.1MB0.5MB/s││Remix).mp3││││↯Qadavre37.7MB/70.0MB3.9MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail41.8/108.1MB·6.75MB/s·[████████░░░░░░░░░░░░░]38%││Donetiesto0││[]@@kpmeh\-2013-\VA-13.7MBDancemaniako320kbps0.46MB/s1││Donethexx0││[]@@kpmeh\-2013-\VA-20.0MBDancemaniako320kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\-2013-\VA-37.4MBDancemaniako-0.46MB/s1││DoneSaveYo245││[]@@kpmeh\-2013-\VA-12.9MBDancemaniako320kbps0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2013-\VA-17.4MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\VA-De14.1MBDancemaniako256kbps0.46MB/s1││││[]@@kpmeh\-2013-\VA-Se16.6MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\VA-Sp18.3MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\VA-Ul14.9MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\Vario54.6MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2013-\Vario22.3MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\Vario15.2MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\Vocal19.8MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2013-\Vocal18.2MBDancemaniako320kbps0.46MB/s1││││›[ ] @@kpmeh\- 2013 -\va - 18.9 MB Dancemaniako 320 kbps 0.46 MB/s 1││││↯Dancemaniako2.2MB/11.1MB3.8MB/s││Remix).mp3││││↯Qadavre39.2MB/70.0MB2.7MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail43.6/108.1MB·8.12MB/s·[████████░░░░░░░░░░░░░]40%││Donetiesto0││[]@@kpmeh\-2014-\ARDI317.4MBDancemaniako320kbps0.46MB/s1││Donethexx0││[]@@kpmeh\-2014-\Armad19.2MBDancemaniako320kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\-2014-\Armad17.5MBDancemaniako320kbps0.46MB/s1││DoneSaveYo245││[]@@kpmeh\-2014-\Armad17.3MBDancemaniako320kbps0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2014-\Armad19.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\Bargr13.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\Beatp19.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\Beatp17.9MBDancemaniako320kbps0.46MB/s1││││›[ ] @@kpmeh\- 2014 -\Beatp 17.6 MB Dancemaniako 320 kbps 0.46 MB/s 1││││↯Dancemaniako2.4MB/11.1MB3.0MB/s││Remix).mp3││││↯Qadavre40.7MB/70.0MB4.8MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail45.3/108.1MB·4.98MB/s·[████████░░░░░░░░░░░░░]41%││Donetiesto0││[]@@kpmeh\-2014-\Ellum16.0MBDancemaniako320kbps0.46MB/s1││Donethexx0││[]@@kpmeh\-2014-\Ellum17.4MBDancemaniako320kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\-2014-\Ellum17.4MBDancemaniako320kbps0.46MB/s1││DoneSaveYo245││[]@@kpmeh\-2014-\Ellum16.8MBDancemaniako320kbps0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2014-\Ellum15.0MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\GusGu17.5MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\Kompa16.5MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\Maceo13.3MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\Maceo17.8MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\Melod20.6MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\Melod18.4MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\Minis17.5MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\Minis20.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\Pete13.8MBDancemaniako320kbps0.46MB/s1││││›[ ] @@kpmeh\- 2014 -\Resid 17.3 MB Dancemaniako 320 kbps 0.46 MB/s 1││││↯Dancemaniako2.6MB/11.1MB0.5MB/s││Remix).mp3││││↯Qadavre42.3MB/70.0MB4.2MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail47.2/108.1MB·5.41MB/s·[█████████░░░░░░░░░░░░]43%││Donetiesto0││[]@@kpmeh\-2014-\VA-13.4MBDancemaniako249kbps0.46MB/s1││Donethexx0││[]@@kpmeh\-2014-\VA-10.0MBDancemaniako320kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\-2014-\VA-17.2MBDancemaniako320kbps0.46MB/s1││DoneSaveYo245││[]@@kpmeh\-2014-\VA-47.0MBDancemaniako-0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2014-\VA-17.6MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\VA-20.2MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\VA-17.3MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\VA-17.4MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\VA-14.9MBDancemaniako275kbps0.46MB/s1││││[]@@kpmeh\-2014-\VA-17.2MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\VA-22.1MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2014-\VA-Az19.0MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2014-\VA-De51.5MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2014-\Vario15.2MBDancemaniako320kbps0.46MB/s1││││›[ ] @@kpmeh\- 2014 -\Vario 15.2 MB Dancemaniako 320 kbps 0.46 MB/s 1││││↯Dancemaniako2.9MB/11.1MB0.5MB/s││Remix).mp3││││↯Qadavre43.8MB/70.0MB5.1MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail49.1/108.1MB·5.33MB/s·[█████████░░░░░░░░░░░░]45%││Donetiesto0││[]@@kpmeh\-2015-\2015\20.5MBDancemaniako320kbps0.46MB/s1││Donethexx0││[]@@kpmeh\-2015-\2015\17.2MBDancemaniako320kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\-2015-\2015\20.5MBDancemaniako320kbps0.46MB/s1││DoneSaveYo245││[]@@kpmeh\-2015-\2015\14.7MBDancemaniako320kbps0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2015-\About49.4MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2015-\Armad17.8MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\Armad8.8MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\BT_Be17.7MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\BT_De15.2MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\BT_Te17.7MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\BT_Te20.6MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\Beatp17.7MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\Beatp17.9MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\Beatp17.5MBDancemaniako320kbps0.46MB/s1││││›[ ] @@kpmeh\- 2015 -\Beatp 17.8 MB Dancemaniako 320 kbps 0.46 MB/s 1││││↯Dancemaniako3.1MB/11.1MB0.5MB/s││Remix).mp3││││↯Qadavre45.5MB/70.0MB4.5MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail50.9/108.1MB·6.48MB/s·[█████████░░░░░░░░░░░░]47%││Donetiesto0││[]@@kpmeh\-2015-\Club18.2MBDancemaniako320kbps0.46MB/s1││Donethexx0││[]@@kpmeh\-2015-\Dance17.1MBDancemaniako320kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\-2015-\Deep11.7MBDancemaniako320kbps0.46MB/s1││DoneSaveYo245││[]@@kpmeh\-2015-\Deep18.2MBDancemaniako320kbps0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2015-\Deep17.3MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\Deep16.7MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\Deep9.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\Deep19.0MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\Deep16.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\Deep16.2MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\Defec16.9MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\Defec17.0MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\Globa54.3MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2015-\Globa47.0MBDancemaniako-0.46MB/s1││││›[ ] @@kpmeh\- 2015 -\Grace 20.8 MB Dancemaniako 320 kbps 0.46 MB/s 1││││↯Dancemaniako3.4MB/11.1MB0.5MB/s││Remix).mp3││││↯Qadavre47.0MB/70.0MB5.7MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail52.9/108.1MB·7.52MB/s·[██████████░░░░░░░░░░░]48%││Donetiesto0││[]@@kpmeh\-2015-\Toolr16.9MBDancemaniako320kbps0.46MB/s1││Donethexx0││[]@@kpmeh\-2015-\VA-17.2MBDancemaniako320kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\-2015-\VA-49.5MBDancemaniako-0.46MB/s1││DoneSaveYo245││[]@@kpmeh\-2015-\VA-46.4MBDancemaniako-0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2015-\VA-17.8MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\VA-47.1MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2015-\VA-18.6MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\VA-54.8MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2015-\VA-9.0MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\VA-20.4MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\VA-20.6MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\VA-19.7MBDancemaniako320kbps0.46MB/s1││││›[ ] @@kpmeh\- 2015 -\VA - 18.5 MB Dancemaniako 320 kbps 0.46 MB/s 1││││↯Dancemaniako3.7MB/11.1MB2.6MB/s││Remix).mp3││││↯Qadavre48.7MB/70.0MB4.6MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail54.7/108.1MB·7.78MB/s·[██████████░░░░░░░░░░░]50%││Donetiesto0││[]@@kpmeh\-2015-\beatp17.7MBDancemaniako320kbps0.46MB/s1││Donethexx0││[]@@kpmeh\-2015-\beatp17.9MBDancemaniako320kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\-2015-\beatp18.0MBDancemaniako320kbps0.46MB/s1││DoneSaveYo245││[]@@kpmeh\-2015-\beatp20.5MBDancemaniako320kbps0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2015-\beatp17.7MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2015-\theb16.9MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2016-\01-2017.8MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2016-\201526.5MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2016-\201651.4MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2016-\201651.6MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2016-\201641.2MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2016-\2016.15.0MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2016-\2016.9.0MBDancemaniako192kbps0.46MB/s1││││[]@@kpmeh\-2016-\2016.10.0MBDancemaniako192kbps0.46MB/s1││││›[ ] @@kpmeh\- 2016 -\BT To 15.2 MB Dancemaniako 320 kbps 0.46 MB/s 1││││↯Dancemaniako3.9MB/11.1MB3.0MB/s││Remix).mp3││││↯Dozel0.5MB/26.9MB0.1MB/s││TechnoHouse&TranceCollection││││↯Qadavre50.2MB/70.0MB4.7MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail56.4/108.1MB·5.2MB/s·[██████████░░░░░░░░░░░]52%││Donetiesto0││[]@@kpmeh\-2016-\Dutch15.1MBDancemaniako320kbps0.46MB/s1││Donethexx0││[]@@kpmeh\-2016-\Minis52.0MBDancemaniako-0.46MB/s1││Donedjbount0││[]@@kpmeh\-2016-\Resid18.8MBDancemaniako320kbps0.46MB/s1││DoneSaveYo245││[]@@kpmeh\-2016-\Spinn15.6MBDancemaniako320kbps0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2016-\Tech18.0MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2016-\Techn20.6MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2016-\Techn18.6MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2016-\Toolr15.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2016-\Top116.3MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2016-\Top117.9MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2016-\Top118.8MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2016-\VA-19.4MBDancemaniako-0.46MB/s1││││›[ ] @@kpmeh\- 2016 -\VA - 19.4 MB Dancemaniako - 0.46 MB/s 1││││↯Dancemaniako4.1MB/11.1MB0.5MB/s││Remix).mp3││││↯Qadavre51.7MB/70.0MB4.4MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail58.3/108.1MB·5.69MB/s·[███████████░░░░░░░░░░]53%││Donetiesto0││[]@@kpmeh\-2016-\remix18.6MBDancemaniako320kbps0.46MB/s1││Donethexx0││[]@@kpmeh\-2017-\100E18.4MBDancemaniako320kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\-2017-\100E18.4MBDancemaniako320kbps0.46MB/s1││DoneSaveYo245││[]@@kpmeh\-2017-\2017.11.0MBDancemaniako192kbps0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2017-\2017.10.8MBDancemaniako192kbps0.46MB/s1││││[]@@kpmeh\-2017-\Beatp0.7MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2017-\Beatp17.9MBDancemaniako320kbps0.46MB/s1││││›[ ] @@kpmeh\- 2017 -\Beatp 16.5 MB Dancemaniako 320 kbps 0.46 MB/s 1││││↯Dancemaniako4.4MB/11.1MB0.5MB/s││Remix).mp3││││↯Qadavre53.4MB/70.0MB5.0MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail60.2/108.1MB·6.04MB/s·[███████████░░░░░░░░░░]55%││Donetiesto0││[]@@kpmeh\-2017-\IP6(212.5MBDancemaniako320kbps0.46MB/s1││Donethexx0││[]@@kpmeh\-2017-\Resid16.2MBDancemaniako320kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\-2017-\Resid18.8MBDancemaniako320kbps0.46MB/s1││DoneSaveYo245││[]@@kpmeh\-2017-\This51.8MBDancemaniako-0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2017-\This17.6MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2017-\This51.8MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2017-\VA-16.6MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2017-\VA-17.4MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2017-\VA-16.7MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2017-\VA-9.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2017-\VA-19.0MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2017-\VA-17.9MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2017-\VA-16.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2017-\VA-16.2MBDancemaniako320kbps0.46MB/s1││││›[ ] @@kpmeh\- 2017 -\VA - 11.0 MB Dancemaniako 320 kbps 0.46 MB/s 1││││↯Dancemaniako4.6MB/11.1MB0.5MB/s││Remix).mp3││││↯Qadavre55.0MB/70.0MB5.3MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail62.0/108.1MB·3.57MB/s·[████████████░░░░░░░░░]57%││Donetiesto0││[]@@kpmeh\-2018-\21PRE13.0MBDancemaniako320kbps0.46MB/s1││Donethexx0││[]@@kpmeh\-2018-\Beatp16.1MBDancemaniako320kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\-2018-\Beatp20.5MBDancemaniako320kbps0.46MB/s1││DoneSaveYo245││[]@@kpmeh\-2018-\Beatp14.5MBDancemaniako320kbps0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2018-\Beatp16.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2018-\Beatp16.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2018-\Beatp13.0MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2018-\Beatp13.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2018-\Beatp14.9MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2018-\Beatp17.2MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2018-\Beatp16.0MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2018-\Indie16.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2018-\July5.9MBDancemaniako128kbps0.46MB/s1││││[]@@kpmeh\-2018-\Maceo36.0MBDancemaniako-0.46MB/s1││││›[ ] @@kpmeh\- 2018 -\Maceo 38.6 MB Dancemaniako - 0.46 MB/s 1││││↯Dancemaniako4.9MB/11.1MB0.5MB/s││Remix).mp3││││↯Dozel0.6MB/26.9MB0.1MB/s││TechnoHouse&TranceCollection││││↯Qadavre56.5MB/70.0MB3.0MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail63.9/108.1MB·9.66MB/s·[████████████░░░░░░░░░]59%││Donetiesto0││[]@@kpmeh\-2018-\TheS37.5MBDancemaniako-0.46MB/s1││Donethexx0││[]@@kpmeh\-2018-\Toolr13.5MBDancemaniako320kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\-2018-\Top516.1MBDancemaniako320kbps0.46MB/s1││DoneSaveYo245││[]@@kpmeh\-2018-\VA-13.4MBDancemaniako320kbps0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2018-\VA-40.6MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2018-\Volum19.5MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2018-\Volum21.3MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2018-\tech16.8MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2019-\Beatp17.4MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2019-\Beatp16.3MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2019-\Beatp14.8MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2019-\Fresh14.3MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2019-\Mayda14.3MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2019-\Mayda17.5MBDancemaniako320kbps0.46MB/s1││││›[ ] @@kpmeh\- 2019 -\TOP N 14.0 MB Dancemaniako 320 kbps 0.46 MB/s 1││││↯Dancemaniako5.1MB/11.1MB0.4MB/s││Remix).mp3││││↯Qadavre58.0MB/70.0MB1.1MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail65.6/108.1MB·3.54MB/s·[████████████░░░░░░░░░]60%││Donetiesto0││[]@@kpmeh\-2021-\VA-17.2MBDancemaniako320kbps0.46MB/s1││Donethexx0││[]@@kpmeh\-2021-\VA-18.9MBDancemaniako320kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\-2021-\VA-Se14.7MBDancemaniako265kbps0.46MB/s1││DoneSaveYo245││[]@@kpmeh\-2021-\defec19.0MBDancemaniako320kbps0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2021-\defec17.2MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2022-\Best19.0MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2022-\Best17.4MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2022-\Defec17.2MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2022-\Defec18.9MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2022-\E-Dan11.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2022-\NewE14.5MBDancemaniako-0.46MB/s1││││[]@@kpmeh\-2022-\VA-19.2MBDancemaniako320kbps0.46MB/s1││││›[ ] @@kpmeh\- 2022 -\VA - 18.9 MB Dancemaniako 320 kbps 0.46 MB/s 1││││↯Dancemaniako5.3MB/11.1MB2.2MB/s││Remix).mp3││││↯Qadavre59.6MB/70.0MB1.1MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail67.4/108.1MB·1.79MB/s·[█████████████░░░░░░░░]62%││Donetiesto0││[]@@kpmeh\-2023-\TECHN18.4MBDancemaniako320kbps0.46MB/s1││Donethexx0││[]@@kpmeh\-2023-\TECHN13.5MBDancemaniako320kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\-2023-\TECHN17.5MBDancemaniako320kbps0.46MB/s1││DoneSaveYo245││[]@@kpmeh\-2023-\VA-20.5MBDancemaniako320kbps0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\-2023-\VA-17.4MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2023-\VA-19.0MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2023-\VA-17.2MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2024-\Defec19.0MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2024-\Defec18.9MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\-2024-\Hard17.9MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\VariousYears13.5MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\VariousYears16.7MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\VariousYears9.1MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\VariousYears17.2MBDancemaniako320kbps0.46MB/s1││││›[ ] @@kpmeh\Various Years 19.0 MB Dancemaniako 320 kbps 0.46 MB/s 1││││↯Dancemaniako5.6MB/11.1MB0.4MB/s││Remix).mp3││││↯Qadavre61.2MB/70.0MB1.1MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail69.3/108.1MB·2.11MB/s·[█████████████░░░░░░░░]64%││Donetiesto0││[]@@kpmeh\VariousYears17.4MBDancemaniako320kbps0.46MB/s1││Donethexx0││[]@@kpmeh\VariousYears18.2MBDancemaniako320kbps0.46MB/s1││Donedjbount0││[]@@kpmeh\VariousYears15.6MBDancemaniako320kbps0.46MB/s1││DoneSaveYo245││[]@@kpmeh\VariousYears17.5MBDancemaniako320kbps0.46MB/s1││›Done Maceo P 612││[]@@kpmeh\VariousYears13.8MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\VariousYears18.2MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\VariousYears18.3MBDancemaniako320kbps0.46MB/s1││││[]@@kpmeh\VariousYears19.4MBDancemaniako320kbps0.46MB/s1││││[]music3\Chromatics-Sh33.1MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ30.6MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ22.2MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ10.9MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ24.9MBcinstitute2-4.4MB/s1││││[]music3\MaceoPlex-DJ33.6MBcinstitute2-4.4MB/s1││││›[ ] music3\Maceo Plex - DJ 32.9 MB cinstitute2 - 4.4 MB/s 1││││↯Dancemaniako5.8MB/11.1MB0.4MB/s││Remix).mp3││││↯Dozel0.6MB/26.9MB0.6MB/s││TechnoHouse&TranceCollection││││↯Qadavre62.8MB/70.0MB1.1MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail71.1/108.1MB·5.61MB/s·[█████████████░░░░░░░░]65%││││›[ ] @@jlksj\new_music\Ali 14.0 MB Qadavre 320 kbps 1.69 MB/s 1││││↯Dancemaniako6.0MB/11.1MB0.4MB/s││Remix).mp3││││↯Qadavre64.4MB/70.0MB4.8MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail72.8/108.1MB·4.47MB/s·[██████████████░░░░░░░]67%││││↯Dancemaniako6.3MB/11.1MB0.4MB/s││Remix).mp3││││↯Qadavre65.8MB/70.0MB3.6MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail75.0/108.1MB·5.85MB/s·[██████████████░░░░░░░]69%││›Done Maceo P 612││›[ ] Music\0day MP3\2026\20 0.0 MB beatrice.songbi - 1.87 MB/s 1││││↯Dancemaniako6.6MB/11.1MB0.6MB/s││Remix).mp3││││↯Qadavre67.7MB/70.0MB4.8MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:3active,2completed,0fail76.9/108.1MB·5.41MB/s·[██████████████░░░░░░░]71%││›Done Maceo P 612││›[✓] Music\0day MP3\2026\20 0.0 MB beatrice.songbi - 1.87 MB/s 1││││↯Dancemaniako6.9MB/11.1MB0.6MB/s││Remix).mp3││││↯Dozel0.7MB/26.9MB0.5MB/s││TechnoHouse&TranceCollection││││↯Qadavre69.3MB/70.0MB4.3MB/s││byLORDEGRAFvol.12││soulseek-rs🦀v7.0.0Downloads:2active,3completed,0fail7.8/38.0MB·0.91MB/s·[████░░░░░░░░░░░░░░░░░]20%││DoneSaveYo245││›[ ] Music\0day MP3\2026\20 0.0 MB beatrice.songbi - 1.87 MB/s 1││││↯Dancemaniako7.1MB/11.1MB0.5MB/s││Remix).mp3││soulseek-rs🦀v7.0.0Downloads:2active,3completed,0fail8.1/38.0MB·0.84MB/s·[████░░░░░░░░░░░░░░░░░]21%││›Done Maceo P 612││›[ ] music3\Maceo Plex - DJ 26.2 MB cinstitute2 - 4.4 MB/s 1││││↯Dancemaniako7.4MB/11.1MB0.5MB/s││Remix).mp3││soulseek-rs🦀v7.0.0Downloads:2active,3completed,0fail8.4/38.0MB·2.22MB/s·[████░░░░░░░░░░░░░░░░░]22%││││›[ ] music3\Maceo Plex - DJ 24.5 MB cinstitute2 - 4.4 MB/s 1││││↯Dancemaniako7.7MB/11.1MB2.1MB/s││Remix).mp3││soulseek-rs🦀v7.0.0Downloads:2active,3completed,0fail8.7/38.0MB·3.93MB/s·[████░░░░░░░░░░░░░░░░░]22%││││›[ ] music3\Maceo Plex - DJ 43.8 MB cinstitute2 - 4.4 MB/s 1││││↯Dancemaniako7.9MB/11.1MB3.6MB/s││Remix).mp3││soulseek-rs🦀v7.0.0Downloads:2active,3completed,0fail8.9/38.0MB·0.83MB/s·[████░░░░░░░░░░░░░░░░░]23%││││[✓]music3\MaceoPlex-DJ43.8MBcinstitute2-4.4MB/s1││││›[✓] music3\Maceo Plex - DJ 26.3 MB cinstitute2 - 4.4 MB/s 1││││↯Dancemaniako8.1MB/11.1MB0.5MB/s││Remix).mp3││soulseek-rs🦀v7.0.0Downloads:2active,3completed,0fail9.4/38.0MB·4.05MB/s·[█████░░░░░░░░░░░░░░░░]24%││││↯Dancemaniako8.5MB/11.1MB3.7MB/s││Remix).mp3││soulseek-rs🦀v7.0.0Downloads:4active,3completed,0fail10.5/73.6MB·7.42MB/s·[██░░░░░░░░░░░░░░░░░░░]14%││││↯Dancemaniako8.8MB/11.1MB2.7MB/s││Remix).mp3││soulseek-rs🦀v7.0.0Downloads:5active,3completed,0fail15.5/117.4MB·33.69MB/s·[██░░░░░░░░░░░░░░░░░░░]13%││││↯Dancemaniako9.1MB/11.1MB1.8MB/s││Remix).mp3││││↯Dozel0.9MB/26.9MB0.3MB/s││TechnoHouse&TranceCollection││││↯cinstitute20.3MB/43.8MB3.3MB/s││Size9.4MB││soulseek-rs🦀v7.0.0Downloads:5active,3completed,0fail26.6/117.4MB·32.45MB/s·[████░░░░░░░░░░░░░░░░░]22%││││↯Dancemaniako9.3MB/11.1MB2.7MB/s││Remix).mp3││││↯cinstitute20.5MB/43.8MB7.2MB/s││Size9.4MB││soulseek-rs🦀v7.0.0Downloads:4active,4completed,0fail17.2/99.3MB·8.19MB/s·[███░░░░░░░░░░░░░░░░░░]17%││││↯Dancemaniako9.6MB/11.1MB2.8MB/s││Remix).mp3││││↯cinstitute20.9MB/43.8MB0.7MB/s││Size9.4MB││soulseek-rs🦀v7.0.0Downloads:5active,4completed,0fail21.5/99.3MB·23.03MB/s·[████░░░░░░░░░░░░░░░░░]21%││││StatUserProgressSpeed││20SaveYourAtoll-││││✓DancemaniakoCompleted-││Mediterranea.flac││││↯Dancemaniako9.8MB/11.1MB1.7MB/s││@@wztnu/MUZYKA/FLAC/TECHNO/The││││⋯DancemaniakoQueued-││GodsPlanet-X(FLACWEB)[The││││›↯ Dozel 0.9 MB/26.9 MB 0.3 MB/s││GodsPlanet]2020-12-10││││✓QadavreCompleted-││││││✓vivioCompleted-││UserDozel││││⋯sillysillysillyQueued-││Size26.9MB││││↯cinstitute23.2MB/43.8MB15.2MB/s││StatusInprogress││soulseek-rs🦀v7.0.0Downloads:4active,5completed,0fail27.1/99.3MB·41.66MB/s·[█████░░░░░░░░░░░░░░░░]27%││││StatUserProgressSpeed││1-15.Jaydee-Jaydee-PP.flac││││✓DancemaniakoCompleted-││music3/MaceoPlex-DJ-Kicks││││↯Dancemaniako10.1MB/11.1MB2.1MB/s││(2013)[CD-FLAC]││││⋯DancemaniakoQueued-││││││↯Dozel0.9MB/26.9MB0.3MB/s││Usercinstitute2││││✓QadavreCompleted-││Size43.8MB││││✓vivioCompleted-││StatusInprogress││││⋯sillysillysillyQueued-││Saveto││││›↯ cinstitute2 6.9 MB/43.8 MB 33.8 MB/s││/Users/micheldegraaf/Downloads/S││││⋯cinstitute2Queued-││oulseek││soulseek-rs🦀v7.0.0Downloads:4active,5completed,0fail32.9/99.3MB·171.54MB/s·[██████░░░░░░░░░░░░░░░]33%││││↯cinstitute210.9MB/43.8MB166.9MB/s││@@kpmeh/-2013-/E-Dance.││││⋯cinstitute2Queued-││UserDancemaniako││││✓beatrice.songbiCompleted-││Size11.1MB││││↯Qadavre10.9MB/17.5MB3.9MB/s││StatusQueued││││⋯QadavreQueued-││Saveto││soulseek-rs🦀v7.0.0Downloads:4active,5completed,0fail45.0/99.3MB·39.69MB/s·[█████████░░░░░░░░░░░░]45%││││StatUserProgressSpeed││113.SaveYourAtoll-El││││✓DancemaniakoCompleted-││Ritual.mp3││││↯Dancemaniako10.5MB/11.1MB0.5MB/s││@@kpmeh/-2021-/ThePlastic││││›⋯ Dancemaniako Queued -││Moon.TechnoSet││││↯Dozel1.0MB/26.9MB0.1MB/s││││││✓QadavreCompleted-││UserDancemaniako││││✓vivioCompleted-││Size16.5MB││││⋯sillysillysillyQueued-││StatusQueued││││↯cinstitute221.3MB/43.8MB34.9MB/s││Saveto││││⋯cinstitute2Queued-││/Users/micheldegraaf/Downloads/S││soulseek-rs🦀v7.0.0Downloads:4active,5completed,0fail60.2/99.3MB·118.74MB/s·[████████████░░░░░░░░░]60%││││StatUserProgressSpeed││9.AzioneAntropica.flac││││✓DancemaniakoCompleted-││@@qxibn/music2024/SaveYour││││↯Dancemaniako10.7MB/11.1MB0.4MB/s││Atoll/SaveYourAtoll-2023-││││⋯DancemaniakoQueued-││TheBigBreath[24bit]││││↯Dozel1.0MB/26.9MB0.5MB/s││││││✓QadavreCompleted-││User││││✓vivioCompleted-││sillysillysilly││││›⋯ sillysillysilly Queued -││Size59.7MB││││↯cinstitute235.0MB/43.8MB114.0MB/s││StatusQueued││soulseek-rs🦀v7.0.0Downloads:3active,6completed,0fail27.0/55.5MB·2.07MB/s·[██████████░░░░░░░░░░░]48%││││StatUserProgressSpeed││1-16.S.A.M.-Nangijala(Maceo││││⋯DancemaniakoQueued-││PlexReggaeEdit).flac││││↯Dozel1.0MB/26.9MB0.5MB/s││music3/MaceoPlex-DJ-Kicks││││✓QadavreCompleted-││(2013)[CD-FLAC]││││⋯sillysillysillyQueued-││Usercinstitute2││││✓cinstitute2Completed-││Size26.3MB││││⋯cinstitute2Queued-││StatusQueued││││✓beatrice.songbiCompleted-││Saveto││││›⋯ cinstitute2 Queued -││/Users/micheldegraaf/Downloads/S││soulseek-rs🦀v7.0.0Downloads:2active,7completed,0fail17.8/44.4MB·5.28MB/s·[████████░░░░░░░░░░░░░]40%││││✓cinstitute2Completed-││@@kpmeh/-2013-/E-Dance.││││⋯cinstitute2Queued-││User││││⋯cinstitute2Queued-││UserQadavre││││✓beatrice.songbiCompleted-││Size23.6MB││││↯Qadavre16.5MB/17.5MB4.1MB/s││StatusQueued││││›⋯ Qadavre Queued -││Saveto││soulseek-rs🦀v7.0.0Downloads:2active,8completed,0fail1.2/53.2MB·1.03MB/s·[░░░░░░░░░░░░░░░░░░░░░]2%││││StatUserProgressSpeed││08.MaceoPlex-StayHigh││││✓cinstitute2Completed-││Baby.flac││││↯cinstitute20.2MB/26.2MB1.0MB/s││@@kpmeh/-2013-/Defected││││✓beatrice.songbiCompleted-││PresentsMostRated2013││││✓QadavreCompleted-││Size54.6MB││││⋯QadavreQueued-││StatusQueued││soulseek-rs🦀v7.0.0Downloads:4active,8completed,0fail3.1/93.2MB·12.57MB/s·[░░░░░░░░░░░░░░░░░░░░░]3%││││✓cinstitute2Completed-││2022.mp3││││↯cinstitute20.9MB/26.2MB7.4MB/s││@@kpmeh/-2013-/E-Dance.││││✓beatrice.songbiCompleted-││ClubbingElectroHouseVol.12││││✓QadavreCompleted-││Size11.1MB││││↯Qadavre1.2MB/23.6MB4.9MB/s││StatusQueued││││›⋯ Dancemaniako Queued -││Saveto││soulseek-rs🦀v7.0.0Downloads:4active,8completed,0fail5.5/93.2MB·4.75MB/s·[█░░░░░░░░░░░░░░░░░░░░]5%││││↯cinstitute21.4MB/26.2MB0.3MB/s││@@jlksj/new_music/EllumAudio││││↯Qadavre3.1MB/23.6MB4.1MB/s││StatusCompleted││soulseek-rs🦀v7.0.0Downloads:4active,8completed,0fail11.6/93.2MB·138.39MB/s·[██░░░░░░░░░░░░░░░░░░░]12%││││↯cinstitute25.4MB/26.2MB132.9MB/s││@@jlksj/new_music/EllumAudio││││↯Qadavre4.6MB/23.6MB6.4MB/s││StatusCompleted││soulseek-rs🦀v7.0.0Downloads:4active,8completed,0fail20.4/93.2MB·228.76MB/s·[████░░░░░░░░░░░░░░░░░]21%││││↯cinstitute212.6MB/26.2MB223.4MB/s││@@jlksj/new_music/EllumAudio││││↯Qadavre6.4MB/23.6MB4.5MB/s││StatusCompleted││soulseek-rs🦀v7.0.0Downloads:4active,8completed,0fail23.6/93.2MB·130.5MB/s·[█████░░░░░░░░░░░░░░░░]25%││││↯cinstitute213.8MB/26.2MB125.7MB/s││@@jlksj/new_music/EllumAudio││││↯Qadavre8.0MB/23.6MB4.0MB/s││StatusCompleted││soulseek-rs🦀v7.0.0Downloads:3active,9completed,0fail11.3/67.0MB·4.72MB/s·[███░░░░░░░░░░░░░░░░░░]16%││││↯Qadavre9.2MB/23.6MB3.9MB/s││StatusCompleted││soulseek-rs🦀v7.0.0Downloads:3active,9completed,0fail13.0/67.0MB·9.41MB/s·[████░░░░░░░░░░░░░░░░░]19%││││↯Qadavre10.7MB/23.6MB4.9MB/s││StatusCompleted││soulseek-rs🦀v7.0.0Downloads:4active,9completed,0fail15.3/93.3MB·12.98MB/s·[███░░░░░░░░░░░░░░░░░░]16%││soulseek-rs🦀v7.0.0Downloads:4active,9completed,0fail17.9/93.3MB·14.03MB/s·[████░░░░░░░░░░░░░░░░░]19%││soulseek-rs🦀v7.0.0Downloads:4active,9completed,0fail21.0/93.3MB·15.71MB/s·[████░░░░░░░░░░░░░░░░░]22%││soulseek-rs🦀v7.0.0Downloads:4active,9completed,0fail26.6/93.3MB·45.35MB/s·[█████░░░░░░░░░░░░░░░░]28%││soulseek-rs🦀v7.0.0Downloads:4active,9completed,0fail39.0/93.3MB·82.45MB/s·[████████░░░░░░░░░░░░░]41%││soulseek-rs🦀v7.0.0Downloads:3active,10completed,0fai24.8/67.0MB·5.38MB/s·[███████░░░░░░░░░░░░░░]37%││soulseek-rs🦀v7.0.0Downloads:3active,10completed,0fai27.0/67.0MB·5.87MB/s·[████████░░░░░░░░░░░░░]40%│││›▏│││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai4.6/43.4MB·0.71MB/s·[██░░░░░░░░░░░░░░░░░░░]10%│││›h▏│││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai4.9/43.4MB·3.01MB/s·[██░░░░░░░░░░░░░░░░░░░]11%│││›hello▏│││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai5.1/43.4MB·2.4MB/s·[██░░░░░░░░░░░░░░░░░░░]11%│││›hellowo▏│││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai5.4/43.4MB·1.62MB/s·[██░░░░░░░░░░░░░░░░░░░]12%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai5.7/43.4MB·2.2MB/s·[██░░░░░░░░░░░░░░░░░░░]13%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai5.9/43.4MB·2MB/s·[██░░░░░░░░░░░░░░░░░░░]13%││Done│04:59→jackass44│Users(327)│1│││›helloworld!▏│││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai6.1/43.4MB·0.76MB/s·[██░░░░░░░░░░░░░░░░░░░]14%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai6.4/43.4MB·2.28MB/s·[███░░░░░░░░░░░░░░░░░░]14%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai6.7/43.4MB·0.91MB/s·[███░░░░░░░░░░░░░░░░░░]15%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai7.0/43.4MB·8.28MB/s·[███░░░░░░░░░░░░░░░░░░]16%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai7.2/43.4MB·3.15MB/s·[███░░░░░░░░░░░░░░░░░░]16%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai7.5/43.4MB·6.2MB/s·[███░░░░░░░░░░░░░░░░░░]17%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai7.7/43.4MB·0.93MB/s·[███░░░░░░░░░░░░░░░░░░]17%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai8.0/43.4MB·4.37MB/s·[███░░░░░░░░░░░░░░░░░░]18%││Done│04:59←stupidmusichoarder│▸ bradybunch668│1││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai8.2/43.4MB·0.45MB/s·[███░░░░░░░░░░░░░░░░░░]18%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai8.5/43.4MB·0.84MB/s·[████░░░░░░░░░░░░░░░░░]19%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai8.8/43.4MB·0.95MB/s·[████░░░░░░░░░░░░░░░░░]20%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai9.1/43.4MB·3.67MB/s·[████░░░░░░░░░░░░░░░░░]20%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai9.4/43.4MB·2.19MB/s·[████░░░░░░░░░░░░░░░░░]21%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai9.6/43.4MB·0.89MB/s·[████░░░░░░░░░░░░░░░░░]22%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai9.9/43.4MB·0.84MB/s·[████░░░░░░░░░░░░░░░░░]22%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai10.1/43.4MB·0.81MB/s·[████░░░░░░░░░░░░░░░░░]23%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai10.4/43.4MB·0.54MB/s·[█████░░░░░░░░░░░░░░░░]23%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai10.7/43.4MB·0.78MB/s·[█████░░░░░░░░░░░░░░░░]24%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai11.0/43.4MB·3.37MB/s·[█████░░░░░░░░░░░░░░░░]25%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai11.2/43.4MB·3.19MB/s·[█████░░░░░░░░░░░░░░░░]25%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai11.5/43.4MB·2.22MB/s·[█████░░░░░░░░░░░░░░░░]26%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai11.7/43.4MB·1.76MB/s·[█████░░░░░░░░░░░░░░░░]26%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai11.9/43.4MB·1.89MB/s·[█████░░░░░░░░░░░░░░░░]27%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai12.2/43.4MB·0.83MB/s·[█████░░░░░░░░░░░░░░░░]28%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai12.5/43.4MB·0.54MB/s·[██████░░░░░░░░░░░░░░░]28%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai12.8/43.4MB·1.13MB/s·[██████░░░░░░░░░░░░░░░]29%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai13.0/43.4MB·1.06MB/s·[██████░░░░░░░░░░░░░░░]30%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai13.3/43.4MB·0.97MB/s·[██████░░░░░░░░░░░░░░░]30%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai13.6/43.4MB·1.02MB/s·[██████░░░░░░░░░░░░░░░]31%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai13.8/43.4MB·7.92MB/s·[██████░░░░░░░░░░░░░░░]31%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai14.1/43.4MB·2.01MB/s·[██████░░░░░░░░░░░░░░░]32%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai14.3/43.4MB·2.31MB/s·[██████░░░░░░░░░░░░░░░]32%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai14.6/43.4MB·0.78MB/s·[███████░░░░░░░░░░░░░░]33%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai14.9/43.4MB·2.39MB/s·[███████░░░░░░░░░░░░░░]34%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai15.2/43.4MB·0.78MB/s·[███████░░░░░░░░░░░░░░]35%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai15.5/43.4MB·3.12MB/s·[███████░░░░░░░░░░░░░░]35%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai15.7/43.4MB·0.81MB/s·[███████░░░░░░░░░░░░░░]36%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai16.0/43.4MB·0.81MB/s·[███████░░░░░░░░░░░░░░]36%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai16.3/43.4MB·0.79MB/s·[███████░░░░░░░░░░░░░░]37%││soulseek-rs🦀v7.0.0Downloads:2active,11completed,0fai16.3/43.4MB·0.54MB/s·[███████░░░░░░░░░░░░░░]37%││[Space→select][Enter→download][b→browseowner][c→chat][/→filter[r→re-index][Esc→close]⟨✉2⟩│ \ No newline at end of file +╭Status────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│soulseek-rs🦀v14.1.2Downloads:0active,0completed,0failed,0que│╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╭[2]Results───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│soulseek-rsv14.1.2││││Noresultsyet.││Pickasearchin[1]Searches,orstartanewone.│╭[1]Searches────────────────────╮╭[3]Downloads/Uploads──────────────────────────────────────────────╮╭[Info]─────────────────────────────────╮│StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││Selectaresultortransferfordetails│││││││╰──────────────────────────────────╯╰─────────────────────────────────────────────────────────────────────╯╰─────────────────────────────────────────╯╭Shortcuts·Sharing:~/Music──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│[s→search][Enter→results][d→remove][m→message][i→inbox][c→chat][b→browseuser][Tab/1-3→pane][z→zoom][w→hide]││[?→keys][q→quit]│╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│search:│╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│[Type→searchterm][←→→movecursor][Backspace/Del→edit][Enter→search][Esc→cancel]││search:public││search:publicdomain││search:publicdomainfield││search:publicdomainfieldrecordings│╭[2]Results:publicdomainfieldrecordings───────────────────────────────────────────────────────────────────────────────────────────────────────╮│soulseek-rsv14.1.2││││Noresultsyet.││Pickasearchin[1]Searches,orstartanewone.│╭[1]Searches────────────────────╮╭[3]Downloads/Uploads──────────────────────────────────────────────╮╭[Info]─────────────────────────────────╮│StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││Selectaresultortransferfordetails││›Activepublic domain fi…│││││││││││╰──────────────────────────────────╯╰─────────────────────────────────────────────────────────────────────╯╰─────────────────────────────────────────╯│[Space→select][Enter→download][b→browseowner][c→chat][/→filter][a/A→selectall/none][h/l→scrollname][g/G→top/end]││[Tab/1-3→pane][z→zoom][w→hide][?→keys][q→quit]││✓FilenameSizeUserBitrateSpeedSlots││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\01 - Coastal D30.5 MBloopdigger675 kbps3.35 MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\02-Reservoir22.6MBloopdigger569kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-Marshat48.0MBloopdigger920kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\04-TramDepo32.2MBloopdigger725kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourB52.5MBloopdigger902kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\06-RainonZ30.4MBloopdigger804kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\cover.jpg0.4MBloopdigger-3.35MB/s1││StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││01-CoastalDawn(SunriseTake,Long││›Activepublic domain fi7││││Version).flac││││││@@gufoo/FieldRecordings/CoastalDawn││││││Sessions(2019)[FLAC24-96]││││││Userloopdigger││││││Size30.5MB││││││Bitrate675kbps││││││Length6m52s││││││Freeslots1available││││││Uploadspeed3.4MB/s││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0117.4MBtapehiss836kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0238.0MBtapehiss741kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0334.6MBtapehiss753kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0418.7MBtapehiss723kbps2.1MB/s2││›Activepublic domain fi11││││Version).flac││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\01-CoastalD30.5MBloopdigger675kbps3.35MB/s1││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\02 - Reservoir22.6 MBloopdigger569 kbps3.35 MB/s1││StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││02-ReservoirWind.flac││›Activepublic domain fi11││││@@gufoo/FieldRecordings/CoastalDawn││││││Size22.6MB││││││Bitrate569kbps││││││Length5m18s││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at48.0 MBloopdigger920 kbps3.35 MB/s1││StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││03-MarshatNight(Long).flac││││││Size48.0MB││││││Bitrate920kbps││││││Length8m21s││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-9.4MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-5.1MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-19.5MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-0.1MBnectoUA-1.05MB/s0││›Activepublic domain fi15││││@@gufoo/FieldRecordings/CoastalDawn││›[✓]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at48.0 MBloopdigger920 kbps3.35 MB/s1││[✓]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-Marshat48.0MBloopdigger920kbps3.35MB/s1││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\04 - Tram Depo32.2 MBloopdigger725 kbps3.35 MB/s1││StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││04-TramDepot,06h.flac││││││Size32.2MB││││││Bitrate725kbps││││││Length6m06s││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour B52.5 MBloopdigger902 kbps3.35 MB/s1││StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││05-HarbourBells.flac││││││Size52.5MB││││││Bitrate902kbps││││││Length9m33s││›[✓]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour B52.5 MBloopdigger902 kbps3.35 MB/s1││›Donepublic domain fi15││││@@gufoo/FieldRecordings/CoastalDawn││soulseek-rs🦀v14.1.2Downloads:0active,0completed,0failed,2que││StatusQueryResults││StatusFilenameUserProgressSpeed││05-HarbourBells.flac││›Donepublic domain fi15││›⋯@@gufoo\FloopdiggerQueued-││@@gufoo/FieldRecordings/CoastalDawn││││⋯@@gufoo\FloopdiggerQueued-││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que2.8/100.5MB·18.61MB/s·[░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]2%│╭[2]Results:publicdomainfieldrecordings───────────────────────────────────────────────────────────────────────────────────────────────────────╮│✓FilenameSizeUserBitrateSpeedSlots││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\01-CoastalD30.5MBloopdigger675kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\02-Reservoir22.6MBloopdigger569kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-Marshat48.0MBloopdigger920kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\04-TramDepo32.2MBloopdigger725kbps3.35MB/s1││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour B52.5 MBloopdigger902 kbps3.35 MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\06-RainonZ30.4MBloopdigger804kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\cover.jpg0.4MBloopdigger-3.35MB/s1││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0117.4MBtapehiss836kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0238.0MBtapehiss741kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0334.6MBtapehiss753kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0418.7MBtapehiss723kbps2.1MB/s2││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-9.4MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-5.1MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-19.5MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-0.1MBnectoUA-1.05MB/s0│╭[1]Searches────────────────────╮╭[3]Downloads/Uploads──────────────────────────────────────────────╮╭[Info]─────────────────────────────────╮│StatusQueryResults││StatusFilenameUserProgressSpeed││03-MarshatNight(Long).flac││›Donepublic domain fi15││›↯@@gufoo\Floopdigger1.3 MB/48.0 MB (2%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger1.5MB/52.5MB(2%)9.7MB/s││Sessions(2019)[FLAC24-96]││││││││││││Userloopdigger││││││Size48.0MB││││││StatusInprogress││││││Saveto││││││/home/demo/Music/Soulseek││││││Bitrate920kbps││││││Length8m21s│╰──────────────────────────────────╯╰─────────────────────────────────────────────────────────────────────╯╰─────────────────────────────────────────╯│[p→pause/resume][x→cancel][r→retryfailed][d→deletequeued/done][c→clearfinished][b→browseuser][Tab/1-3→pane][z→zoom]││[w→hide][?→keys][q→quit]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que5.6/100.5MB·18.61MB/s·[█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]5%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger2.7 MB/48.0 MB (5%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger2.9MB/52.5MB(5%)9.7MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que11.2/100.5MB·18.61MB/s·[███░░░░░░░░░░░░░░░░░░░░░░░░░░░░]11%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger5.3 MB/48.0 MB (11%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger5.8MB/52.5MB(11%)9.7MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que19.5/100.5MB·18.61MB/s·[██████░░░░░░░░░░░░░░░░░░░░░░░░░]19%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger9.3 MB/48.0 MB (19%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger10.2MB/52.5MB(19%)9.7MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que25.1/100.5MB·18.61MB/s·[███████░░░░░░░░░░░░░░░░░░░░░░░░]25%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger12.0 MB/48.0 MB (25%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger13.1MB/52.5MB(25%)9.7MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que30.7/100.5MB·18.61MB/s·[█████████░░░░░░░░░░░░░░░░░░░░░░]30%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger14.7 MB/48.0 MB (30%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger16.0MB/52.5MB(30%)9.7MB/s││Sessions(2019)[FLAC24-96]││›Donepublic domain fi15││›↯@@gufoo\Floopdigger17.3 MB/48.0 MB (36%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger20.4MB/52.5MB(38%)9.7MB/s││Sessions(2019)[FLAC24-96]││›Donepublic domain fi15││›↯@@gufoo\Floopdigger18.7 MB/48.0 MB (38%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↑↯HarbourBcrateminer5.3MB/41.0MB(12%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que41.9/100.5MB·18.61MB/s·[████████████░░░░░░░░░░░░░░░░░░░]41%│╭[3]Downloads/Uploads─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│StatusFilenameUserProgressSpeed││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger20.0 MB/48.0 MB (41%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger21.9MB/52.5MB(41%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer5.7MB/41.0MB(13%)3.7MB/s││[p→pause/resume][x→cancel][r→retryfailed][d→deletequeued/done][c→clearfinished][b→browseuser][Tab/1-3→pane][z→unzoom]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que50.2/100.5MB·18.61MB/s·[███████████████░░░░░░░░░░░░░░░░]50%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger24.0 MB/48.0 MB (50%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger26.2MB/52.5MB(50%)9.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que55.8/100.5MB·18.61MB/s·[█████████████████░░░░░░░░░░░░░░]55%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger26.7 MB/48.0 MB (55%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger29.2MB/52.5MB(55%)9.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que61.4/100.5MB·18.61MB/s·[██████████████████░░░░░░░░░░░░░]61%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger29.3 MB/48.0 MB (61%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger32.1MB/52.5MB(61%)9.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que67.0/100.5MB·18.61MB/s·[████████████████████░░░░░░░░░░░]66%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger32.0 MB/48.0 MB (66%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger35.0MB/52.5MB(66%)9.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que72.6/100.5MB·18.61MB/s·[██████████████████████░░░░░░░░░]72%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger34.7 MB/48.0 MB (72%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger37.9MB/52.5MB(72%)9.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que81.0/100.5MB·18.61MB/s·[████████████████████████░░░░░░░]80%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger38.7 MB/48.0 MB (80%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger42.3MB/52.5MB(80%)9.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que83.8/100.5MB·18.61MB/s·[█████████████████████████░░░░░░]83%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger40.0 MB/48.0 MB (83%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger43.8MB/52.5MB(83%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer14.0MB/41.0MB(34%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que89.3/100.5MB·18.61MB/s·[███████████████████████████░░░░]88%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger42.7 MB/48.0 MB (88%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger46.7MB/52.5MB(88%)9.7MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que94.9/100.5MB·18.61MB/s·[█████████████████████████████░░]94%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger45.3 MB/48.0 MB (94%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger49.6MB/52.5MB(94%)9.7MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que97.7/100.5MB·18.61MB/s·[██████████████████████████████░]97%││StatusQueryResults││StatusFilenameUserProgressSpeed││03-MarshatNight(Long).flac││›Donepublic domain fi15││›↯@@gufoo\Floopdigger46.7 MB/48.0 MB (97%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger51.0MB/52.5MB(97%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer17.0MB/41.0MB(41%)3.7MB/s││││││││Userloopdigger││││││Size48.0MB││││││StatusInprogress││││││Saveto││││││/home/demo/Music/Soulseek││││││Bitrate920kbps││││││Length8m21s││soulseek-rs🦀v14.1.2Downloads:0active,2completed,0failed,0que││›Donepublic domain fi15││›✓@@gufoo\FloopdiggerCompleted-││@@gufoo/FieldRecordings/CoastalDawn││││✓@@gufoo\FloopdiggerCompleted-││Sessions(2019)[FLAC24-96]││││││StatusCompleted││││↑↯HarbourBcrateminer19.9MB/41.0MB(48%)3.7MB/s│││╭[3]Downloads/Uploads─────────────────────────────────────────────────────────────────────╮╭[Info]──────────────────────────────────────────────╮│StatusFilenameUserProgressSpeed││05-HarbourBells.flac││›✓@@gufoo\Field Recordings\CoastalloopdiggerCompleted-││@@gufoo/FieldRecordings/CoastalDawnSessions││✓@@gufoo\FieldRecordings\CoastalloopdiggerCompleted-││(2019)[FLAC24-96]││↑↯HarbourBells(LiveattheQuay)crateminer20.3MB/41.0MB(49%)3.7MB/s││││││Userloopdigger││││Size52.5MB││││Bitrate902kbps││││Length9m33s││││││││Freeslots1available││││Uploadspeed3.4MB/s│╰────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────╯│↑↯HarbourBells(LiveattheQuay)crateminer27.6MB/41.0MB(67%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer29.2MB/41.0MB(71%)3.7MB/s││││[]…eldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\01-CoastalDawn(Sunr30.5MBloopdigger675kbps3.35MB/s1││[]…eldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\02-ReservoirWind.fla22.6MBloopdigger569kbps3.35MB/s1││[]…eldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(Lo48.0MBloopdigger920kbps3.35MB/s1││[]…eldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\04-TramDepot,06h.fl32.2MBloopdigger725kbps3.35MB/s1││›[ ]…eld Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flac52.5 MBloopdigger902 kbps3.35 MB/s1││[]…eldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\06-RainonZincRoof.30.4MBloopdigger804kbps3.35MB/s1││[]…less\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\01-EmptyC17.4MBtapehiss836kbps2.1MB/s2││[]…less\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\02-Forest38.0MBtapehiss741kbps2.1MB/s2││[]…less\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\03-Quarry34.6MBtapehiss753kbps2.1MB/s2││[]…less\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\04-NightF18.7MBtapehiss723kbps2.1MB/s2││[]…mplete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-BY)\01-9.4MBnectoUA320kbps1.05MB/s0││[]…mplete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-BY)\02-5.1MBnectoUA320kbps1.05MB/s0││[]…mplete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-BY)\03-19.5MBnectoUA320kbps1.05MB/s0││[]…mplete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-BY)\readm0.1MBnectoUA-1.05MB/s0││↑↯HarbourBells(LiveattheQuay)crateminer35.3MB/41.0MB(86%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer38.2MB/41.0MB(93%)3.7MB/s││││││↑↯HarbourBcrateminer38.5MB/41.0MB(94%)3.7MB/s││││││↑✓HarbourBcrateminerCompleted-││││›[ ]@@gufoo\Field Recor╭Keys─────────────────────────────────────────────────────────────────────────────────────────╮bps3.35 MB/s1││[]@@gufoo\FieldRecor│PanesSearches│bps3.35MB/s1││[]@@gufoo\FieldRecor│Tab/Shift-Tabnext/previouspaneEntershowitsresults│3.35MB/s1││[]Music\lossless\Publ│123focusapane,hiddenornotdremovethesearch│bps2.1MB/s2││[]Music\lossless\Publ│zzoomthefocusedpaneCcleareverysearch│bps2.1MB/s2││[]Music\lossless\Publ│whidethefocusedpanecchatrooms│bps2.1MB/s2││[]Music\lossless\Publ│Escleavezoom│bps2.1MB/s2││[]@@hnttf\complete\Ex│clickfocusapaneResults│bps1.05MB/s0││[]@@hnttf\complete\Ex│Spaceselect/deselect│bps1.05MB/s0││[]@@hnttf\complete\Ex│Anywherea/Aselectall/none│bps1.05MB/s0││[]@@hnttf\complete\Ex│ssearchthenetworkEnterdownloadtheselection│1.05MB/s0│││bbrowseauser'sfiles/filter,Enterkeepsit,Escclears││││mcomposeaprivatemessagehl/←→scrollthefilename││││iinbox0/$start/endofthefilename││││osettingsbbrowsetheowner││╰─────────────────────────│?thislistcchatrooms│──────────────────────────╯╭[1]Searches───────────│qquit│──────────────────────────╮│StatusQuery│Downloads/Uploads│Night(Long).flac││›Donepublic domain fi│Anylistppause/resume│Recordings/CoastalDawn│││↑↓/kjmoveonerowxcancel│)[FLAC24-96]│││HomeEnd/gGfirst/lastrowrretryafailedone││││PgUpPgDn/^b^fonepageddeleteaqueuedorfinishedone│loopdigger│││^u^dhalfapageccleareveryfinishedone│48.0MB││╰───────────────────────────────────────────────────────────────────────────────────────────────╯Completed││[?/Esc→close]│╰────────────────────────────────────────────────────────────────╭[1]Searches────────────────────╮╭[3]Downloads/Uploads───│││││╭Shortcuts·Sharing:~/Music───────────────────────────────────────────────────────────────────────────│[s→search][Enter→results][d→remove][m→message][i→inbox][c→chat][b→browseuser][Tab/1-3→pane│StatusQueryResults╮╭[3]Downloads/Uploads──────────────────────────────────────────────╮╭[Info]─────────────────────────────────╮╭│││││╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│[Type→searchterm][←→→movecursor][Backspace/Del→edit][Enter→search]││search:p││search:pu││search:pub││search:publ││search:publi││search:publicd││search:publicdo││search:publicdom││search:publicdoma││search:publicdomai││search:publicdomainf││search:publicdomainfi││search:publicdomainfie││search:publicdomainfiel││search:publicdomainfieldr││search:publicdomainfieldre││search:publicdomainfieldrec││search:publicdomainfieldreco││search:publicdomainfieldrecor││search:publicdomainfieldrecord││search:publicdomainfieldrecordi││search:publicdomainfieldrecordin││search:publicdomainfieldrecording│╰────────────s────────────────────╮╭[3]Downloads/Uploads──────────────────────────────────────────────╮╭[Info]─────────────────────────────────╮│StatusQueryResults│││││││││││╰──────────────────────────────────╯╰──────────────────────││[Space→select][Enter→download]───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│[Tab/1-3→pane][z][←→→movecursor][Backspace/Del→edit][Enter→search][Esc→cancel]││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2││││││Length││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\041││›Activepublic domain fi11││││@@gufoo/FieldRecordings/CoastalDa││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-0││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0418.7MBtapehiss723kbps2.1MB/s2││›Donepublic domain fi15││›↯@@gufoo\FloopdiggerQueued-││@@gufoo/FieldRecordings/CoastalDawn││││││Length3.4MB/s││[p→pause/resume][x→cancel][r→retryfailed][d→deletequeued/done][c→clearfinished][one][h/l→scrollname][g/G→top/end]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que8.4/100.5MB·18.61MB/s·[██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]8%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger4.0 MB/48.0 MB (8%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger4.4MB/52.5MB(8%)9.7MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que14.0/100.5MB·18.61MB/s·[████░░░░░░░░░░░░░░░░░░░░░░░░░░░]13%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger6.7 MB/48.0 MB (13%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger7.3MB/52.5MB(13%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer0.2MB/41.0MB(0%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que16.8/100.5MB·18.61MB/s·[█████░░░░░░░░░░░░░░░░░░░░░░░░░░]16%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger8.0 MB/48.0 MB (16%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger8.8MB/52.5MB(16%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer0.6MB/41.0MB(1%)3.7MB/s││││││↑↯HarbourBcrateminer1.0MB/41.0MB(2%)3.7MB/s││││││↑↯HarbourBcrateminer1.4MB/41.0MB(3%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que22.3/100.5MB·18.61MB/s·[██████░░░░░░░░░░░░░░░░░░░░░░░░░]22%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger10.7 MB/48.0 MB (22%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger11.7MB/52.5MB(22%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer1.7MB/41.0MB(4%)3.7MB/s││││││↑↯HarbourBcrateminer2.1MB/41.0MB(5%)3.7MB/s││││││↑↯HarbourBcrateminer2.5MB/41.0MB(6%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que27.9/100.5MB·18.61MB/s·[████████░░░░░░░░░░░░░░░░░░░░░░░]27%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger13.3 MB/48.0 MB (27%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger14.6MB/52.5MB(27%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer2.9MB/41.0MB(7%)3.7MB/s││││││↑↯HarbourBcrateminer3.3MB/41.0MB(8%)3.7MB/s││││││↑↯HarbourBcrateminer3.7MB/41.0MB(9%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que33.5/100.5MB·18.61MB/s·[██████████░░░░░░░░░░░░░░░░░░░░░]33%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger16.0 MB/48.0 MB (33%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger17.5MB/52.5MB(33%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer4.1MB/41.0MB(9%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que36.3/100.5MB·18.61MB/s·[███████████░░░░░░░░░░░░░░░░░░░░]36%││││↯@@gufoo\Floopdigger19.0MB/52.5MB(36%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer4.5MB/41.0MB(10%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que37.7/100.5MB·18.61MB/s·[███████████░░░░░░░░░░░░░░░░░░░░]37%││││↑↯HarbourBcrateminer4.9MB/41.0MB(11%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que39.1/100.5MB·18.61MB/s·[████████████░░░░░░░░░░░░░░░░░░░]38%││›↯@@gufoo\ldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\01-CoastalD30.5MBloopdigger675kbps3.35MB/s1││↑↯HarbourBells(LiveattheQuay).flaccrateminer5.7MB/41.0MB(13%)3.7MB/s││FieldRecordings,Vol.2(CreativeCommonsZero)\0117.4MBtapehiss836kbps2.1MB/s2││dings2004(netlabel,CC-9.4MBnectoUA320kbps1.05MB/s0││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│││↑↯HarbourBcrateminer5.3MB/41.0MB(12%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay).flaccrateminer6.0MB/41.0MB(14%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que44.7/100.5MB·18.61MB/s·[█████████████░░░░░░░░░░░░░░░░░░]44%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger21.3 MB/48.0 MB (44%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger23.3MB/52.5MB(44%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer6.4MB/41.0MB(15%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que47.5/100.5MB·18.61MB/s·[██████████████░░░░░░░░░░░░░░░░░]47%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger22.7 MB/48.0 MB (47%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger24.8MB/52.5MB(47%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer6.8MB/41.0MB(16%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer7.1MB/41.0MB(17%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer7.5MB/41.0MB(18%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que53.0/100.5MB·18.61MB/s·[████████████████░░░░░░░░░░░░░░░]52%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger25.3 MB/48.0 MB (52%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger27.7MB/52.5MB(52%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer7.9MB/41.0MB(19%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer8.3MB/41.0MB(20%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer8.7MB/41.0MB(21%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que58.6/100.5MB·18.61MB/s·[██████████████████░░░░░░░░░░░░░]58%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger28.0 MB/48.0 MB (58%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger30.6MB/52.5MB(58%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer9.1MB/41.0MB(22%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer9.4MB/41.0MB(23%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer9.8MB/41.0MB(23%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que64.2/100.5MB·18.61MB/s·[███████████████████░░░░░░░░░░░░]63%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger30.7 MB/48.0 MB (63%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger33.5MB/52.5MB(63%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer10.2MB/41.0MB(24%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer10.6MB/41.0MB(25%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer11.0MB/41.0MB(26%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que69.8/100.5MB·18.61MB/s·[█████████████████████░░░░░░░░░░]69%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger33.3 MB/48.0 MB (69%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger36.5MB/52.5MB(69%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer11.4MB/41.0MB(27%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer11.7MB/41.0MB(28%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer12.1MB/41.0MB(29%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que75.4/100.5MB·18.61MB/s·[███████████████████████░░░░░░░░]75%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger36.0 MB/48.0 MB (75%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger39.4MB/52.5MB(75%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer12.5MB/41.0MB(30%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que78.2/100.5MB·18.61MB/s·[████████████████████████░░░░░░░]77%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger37.3 MB/48.0 MB (77%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger40.8MB/52.5MB(77%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer12.9MB/41.0MB(31%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer13.3MB/41.0MB(32%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer13.7MB/41.0MB(33%)3.7MB/s││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\06-RainonZ││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0238.0MBtapehiss741kbps││[]@@hnttf\compl│╰──────────────────────────────││StatusQueryResults││││││││↑↯HarbourBcrateminer14.2MB/41.0MB(34%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que86.5/100.5MB·18.61MB/s·[██████████████████████████░░░░░]86%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger41.3 MB/48.0 MB (86%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger45.2MB/52.5MB(86%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer14.6MB/41.0MB(35%)3.7MB/s││││││↑↯HarbourBcrateminer15.0MB/41.0MB(36%)3.7MB/s││││││↑↯HarbourBcrateminer15.4MB/41.0MB(37%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que92.1/100.5MB·18.61MB/s·[████████████████████████████░░░]91%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger44.0 MB/48.0 MB (91%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger48.1MB/52.5MB(91%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer15.8MB/41.0MB(38%)3.7MB/s││││││↑↯HarbourBcrateminer16.2MB/41.0MB(39%)3.7MB/s││││││↑↯HarbourBcrateminer16.6MB/41.0MB(40%)3.7MB/s││││││↑↯HarbourBcrateminer16.6MB/41.0MB(40%)3.7MB/s│││╰──────────────────────────────────╯╰─────────────────────────────────────────────────────────────────────╯╰─────────────────────────────────────────╯│[s→search][Enter→results][d→remove][m→message][i→inbox]done][c→clearfinished][b→browseuser][Tab/1-3→pane][z→zoom]││││↑↯HarbourBcrateminer17.2MB/41.0MB(41%)3.7MB/s││││││↑↯HarbourBcrateminer17.6MB/41.0MB(42%)3.7MB/s││││││↑↯HarbourBcrateminer18.0MB/41.0MB(43%)3.7MB/s││││││↑↯HarbourBcrateminer18.4MB/41.0MB(44%)3.7MB/s││││││↑↯HarbourBcrateminer18.8MB/41.0MB(45%)3.7MB/s││││││↑↯HarbourBcrateminer19.2MB/41.0MB(46%)3.7MB/s││││││↑↯HarbourBcrateminer19.5MB/41.0MB(47%)3.7MB/s│││╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│StatusFilenameUserProgressSpeed││05d││03-MarshatNight(Long).flac││✓@@gufoo\FieldRecordings\CoastalloopdiggerCompleted-││(2019)[FLAC24-9ions(2019)[FLAC24-96]││││StatusCompleted││││Uploadspeed8m21s││[Space→select][Enter→download][b→browseowner][c[i→inbox][c→chat][b→browseuser][Tab/1-3→pane][z→zoom][w→hide]││[Tab/1-3→pane][z→zoom][w→hide][?→││↑↯HarbourBells(LiveattheQuay)crateminer20.6MB/41.0MB(50%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer21.0MB/41.0MB(51%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer21.4MB/41.0MB(52%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer21.8MB/41.0MB(53%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer22.2MB/41.0MB(54%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer22.5MB/41.0MB(54%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer22.9MB/41.0MB(55%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer23.3MB/41.0MB(56%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer23.7MB/41.0MB(57%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer24.1MB/41.0MB(58%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer24.5MB/41.0MB(59%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer24.9MB/41.0MB(60%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer25.3MB/41.0MB(61%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer25.7MB/41.0MB(62%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer26.1MB/41.0MB(63%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer26.5MB/41.0MB(64%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer26.8MB/41.0MB(65%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer27.2MB/41.0MB(66%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer28.0MB/41.0MB(68%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer28.4MB/41.0MB(69%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer28.8MB/41.0MB(70%)3.7MB/s││││[]…less\PublicDomainFieldRecordings,Vol.2(CreativeCreativeCommonsZero)\0334.6MBtapehiss753kbps2.1MB/s2││↑↯HarbourBells(LiveattheQuay)crateminer29.6MB/41.0MB(72%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer29.8MB/41.0MB(72%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer30.2MB/41.0MB(73%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer30.6MB/41.0MB(74%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer31.0MB/41.0MB(75%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer31.4MB/41.0MB(76%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer31.8MB/41.0MB(77%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer32.2MB/41.0MB(78%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer32.6MB/41.0MB(79%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer33.0MB/41.0MB(80%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer33.4MB/41.0MB(81%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer33.8MB/41.0MB(82%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer34.1MB/41.0MB(83%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer34.5MB/41.0MB(84%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer34.9MB/41.0MB(85%)3.7MB/s││││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommons-Quarry34.6MBtapehiss753kbps2.1MB/s2││↑↯HarbourBells(LiveattheQuay)crateminer35.7MB/41.0MB(87%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer35.8MB/41.0MB(87%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer36.2MB/41.0MB(88%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer36.6MB/41.0MB(89%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer37.0MB/41.0MB(90%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer37.4MB/41.0MB(91%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer37.8MB/41.0MB(92%)3.7MB/s│││╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│││││Userger││[sace→select][Enter→download][b→browseowner][c→chat][/→filter][a/A→selectall/none][h/l→scrollname][g/G→top/end]││[s→search][Enter→results][d→remove][m→message][i→inbox][c→chat][b→browseuser][Tab/1-3→pane][zame][g/G→top/end]││││↑↯HarbourBcrateminer38.8MB/41.0MB(94%)3.7MB/s││││││↑↯HarbourBcrateminer39.2MB/41.0MB(95%)3.7MB/s││││││↑↯HarbourBcrateminer39.6MB/41.0MB(96%)3.7MB/s││││││↑↯HarbourBcrateminer40.0MB/41.0MB(97%)3.7MB/s││││││↑↯HarbourBcrateminer40.4MB/41.0MB(98%)3.7MB/s││││││↑↯HarbourBcrateminer40.7MB/41.0MB(99%)3.7MB/s││││[]@@hnttf\complete\Ex│clickfocusapaneResults320kbps1.05MB/s0│││m│╰─────────────────────────│?thislist──────────────────────────────╮╭[Info]─────────────────────────────────╮│›││✓@@gufoo\FloopdiggerCompleted-││Sessions(2019)[FLAC24-96]│││HomeEnd/gGfirst/lastrowrretryafailedone│loopdigger││││││Savetoo/Music/Soulseek│╭Shortcuts·Sharing:~/Music────────────────────────────────────────────────────────────────────────────────────e][z→zoom][w→hide]││[]Music\lossless\PublicDomainzoomthefocusedpaneCcleareverysearch│bps2.1MB/s2││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-5.1MBnectoUA│bps1.05MB/s0│╰────────────────────────────────────────────────────────bbrowsetheowner│││listppause/resume│Recordings/CoastalDawn│││n/^b^fonepageddeleteaqueuedorfinishedone│loopdigger│╰────────││││││[s→search][Enter→results][d→remove][m────────────────────────────────────────────────────────────────────────────────────────────────────╮│[?→keys][qe]│ \ No newline at end of file diff --git a/soulseek-rs/src/models/app_state.rs b/soulseek-rs/src/models/app_state.rs index 8cf44f2..c0b6061 100644 --- a/soulseek-rs/src/models/app_state.rs +++ b/soulseek-rs/src/models/app_state.rs @@ -39,6 +39,75 @@ pub enum FocusedPane { Downloads, } +impl FocusedPane { + /// Every focusable pane, in the order `Tab` walks them: the number each + /// carries in its legend. + pub const ALL: [Self; 3] = [Self::Searches, Self::Results, Self::Downloads]; + + const fn index(self) -> usize { + match self { + Self::Searches => 0, + Self::Results => 1, + Self::Downloads => 2, + } + } +} + +/// Which panes are on screen. A hidden pane gives its space to the others; a +/// zoomed one takes the whole content area for itself, so `Tab` through the +/// panes while zoomed reads as switching windows. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct PaneLayout { + hidden: [bool; 3], + pub zoomed: bool, +} + +impl PaneLayout { + #[must_use] + pub const fn is_visible(&self, pane: FocusedPane) -> bool { + !self.hidden[pane.index()] + } + + /// The panes still on screen, in `Tab` order. + pub fn visible(&self) -> impl Iterator + '_ { + FocusedPane::ALL + .into_iter() + .filter(move |pane| self.is_visible(*pane)) + } + + /// Take `pane` off the screen. Refuses when it is the last one showing — + /// an empty window has nothing to press a key in — and says so. + pub fn hide(&mut self, pane: FocusedPane) -> bool { + if self.visible().count() <= 1 { + return false; + } + self.hidden[pane.index()] = true; + true + } + + pub const fn show(&mut self, pane: FocusedPane) { + self.hidden[pane.index()] = false; + } + + /// The visible pane after (or before) `from`, wrapping around. `from` + /// itself may already be hidden, which is how focus leaves a pane that + /// was just hidden. + #[must_use] + pub fn neighbour(&self, from: FocusedPane, forward: bool) -> FocusedPane { + let count = FocusedPane::ALL.len(); + let step = if forward { 1 } else { count - 1 }; + let mut index = from.index(); + for _ in 0..count { + index = (index + step) % count; + let candidate = FocusedPane::ALL[index]; + if self.is_visible(candidate) { + return candidate; + } + } + from + } +} + /// What the shared command bar is currently capturing input for. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CommandBarMode { @@ -92,6 +161,12 @@ pub struct AppState { // UI State pub focused_pane: FocusedPane, + pub layout: PaneLayout, + /// The keys overlay (`?`) is open. + pub show_help: bool, + /// Rows the keys overlay is scrolled down, for a terminal too short to + /// show it whole. The renderer clamps it to what there is to scroll. + pub help_scroll: usize, pub should_exit: bool, pub command_bar_active: bool, pub command_bar_input: String, @@ -164,6 +239,9 @@ impl AppState { active_downloads_count: 0, focused_pane: FocusedPane::Searches, + layout: PaneLayout::default(), + show_help: false, + help_scroll: 0, should_exit: false, command_bar_active: false, command_bar_input: String::new(), @@ -195,6 +273,31 @@ impl AppState { } } + /// Put the focus on `pane`, bringing it back on screen if it was hidden: + /// pressing a pane's number is also how it comes back. + pub const fn focus_pane(&mut self, pane: FocusedPane) { + self.layout.show(pane); + self.focused_pane = pane; + } + + /// Move the focus to the next (or previous) visible pane. + pub fn cycle_focus(&mut self, forward: bool) { + self.focused_pane = self.layout.neighbour(self.focused_pane, forward); + } + + /// Hide the focused pane and move the focus to its neighbour. The last + /// visible pane stays; there would be nowhere for the focus to go. + pub fn hide_focused_pane(&mut self) { + let pane = self.focused_pane; + if self.layout.hide(pane) { + self.focused_pane = self.layout.neighbour(pane, true); + } + } + + pub const fn toggle_zoom(&mut self) { + self.layout.zoomed = !self.layout.zoomed; + } + /// Open the chat popup on a conversation with `peer`, ready to type. pub fn open_chat(&mut self, peer: String) { self.chat_peer = Some(peer); @@ -290,6 +393,99 @@ mod tests { assert_eq!(state.active_chat_peer(), Some("bob")); } + fn visible(layout: &PaneLayout) -> Vec { + layout.visible().collect() + } + + #[test] + fn every_pane_starts_visible_and_unzoomed() { + let state = AppState::new(); + assert_eq!(visible(&state.layout), FocusedPane::ALL); + assert!(!state.layout.zoomed); + assert!(!state.show_help); + } + + #[test] + fn tab_cycles_the_focus_through_the_visible_panes_and_wraps() { + let mut state = AppState::new(); + assert_eq!(state.focused_pane, FocusedPane::Searches); + state.cycle_focus(true); + assert_eq!(state.focused_pane, FocusedPane::Results); + state.cycle_focus(true); + assert_eq!(state.focused_pane, FocusedPane::Downloads); + state.cycle_focus(true); + assert_eq!(state.focused_pane, FocusedPane::Searches, "wraps"); + state.cycle_focus(false); + assert_eq!(state.focused_pane, FocusedPane::Downloads, "and back"); + } + + #[test] + fn hiding_the_focused_pane_moves_the_focus_past_it() { + let mut state = AppState::new(); + state.focus_pane(FocusedPane::Results); + state.hide_focused_pane(); + assert_eq!( + visible(&state.layout), + [FocusedPane::Searches, FocusedPane::Downloads] + ); + assert_eq!(state.focused_pane, FocusedPane::Downloads); + + // Tab now skips the hidden pane in both directions. + state.cycle_focus(true); + assert_eq!(state.focused_pane, FocusedPane::Searches); + state.cycle_focus(false); + assert_eq!(state.focused_pane, FocusedPane::Downloads); + } + + #[test] + fn hiding_wraps_the_focus_to_the_first_pane() { + let mut state = AppState::new(); + state.focus_pane(FocusedPane::Downloads); + state.hide_focused_pane(); + assert_eq!(state.focused_pane, FocusedPane::Searches); + } + + #[test] + fn the_last_visible_pane_cannot_be_hidden() { + let mut state = AppState::new(); + state.hide_focused_pane(); + state.hide_focused_pane(); + assert_eq!(visible(&state.layout), [FocusedPane::Downloads]); + state.hide_focused_pane(); + assert_eq!( + visible(&state.layout), + [FocusedPane::Downloads], + "one pane always stays on screen" + ); + assert_eq!(state.focused_pane, FocusedPane::Downloads); + } + + #[test] + fn focusing_a_hidden_pane_by_number_brings_it_back() { + let mut state = AppState::new(); + state.focus_pane(FocusedPane::Results); + state.hide_focused_pane(); + assert!(!state.layout.is_visible(FocusedPane::Results)); + + state.focus_pane(FocusedPane::Results); + + assert!(state.layout.is_visible(FocusedPane::Results)); + assert_eq!(state.focused_pane, FocusedPane::Results); + } + + #[test] + fn zoom_toggles_and_leaves_hidden_panes_alone() { + let mut state = AppState::new(); + state.focus_pane(FocusedPane::Searches); + state.hide_focused_pane(); + state.toggle_zoom(); + assert!(state.layout.zoomed); + assert!(!state.layout.is_visible(FocusedPane::Searches)); + state.toggle_zoom(); + assert!(!state.layout.zoomed); + assert!(!state.layout.is_visible(FocusedPane::Searches)); + } + #[test] fn opening_a_chat_pins_a_peer_with_no_messages_yet() { let mut state = AppState::new(); diff --git a/soulseek-rs/src/models/mod.rs b/soulseek-rs/src/models/mod.rs index f62e346..554639d 100644 --- a/soulseek-rs/src/models/mod.rs +++ b/soulseek-rs/src/models/mod.rs @@ -6,7 +6,7 @@ mod settings; pub use app_state::{ AppState, ChatMessage, CommandBarMode, DownloadEntry, FocusedPane, - MessageDirection, SearchEntry, SearchStatus, + MessageDirection, PaneLayout, SearchEntry, SearchStatus, }; pub use browse::{ BrowseState, BrowseStatus, BrowseTabs, files_under, find_node, diff --git a/soulseek-rs/src/ui/main_tui/input.rs b/soulseek-rs/src/ui/main_tui/input.rs index 444f8b2..4d7871d 100644 --- a/soulseek-rs/src/ui/main_tui/input.rs +++ b/soulseek-rs/src/ui/main_tui/input.rs @@ -9,6 +9,8 @@ use ratatui::layout::Position; use ratatui::widgets::TableState; const NAME_SCROLL_STEP: isize = 8; +/// Rows a page key moves the keys overlay, which has no table to measure. +const HELP_PAGE: usize = 10; impl MainTui { pub(super) fn handle_key_event(&mut self, key: KeyEvent) { @@ -17,6 +19,27 @@ impl MainTui { return self.handle_command_bar_input(key); } + // The keys list closes on the key that opened it, or the usual two, + // and scrolls like any list for a terminal too short to show it all. + if self.state.show_help { + let scroll = &mut self.state.help_scroll; + match key.code { + KeyCode::Char('?' | 'q') | KeyCode::Esc => { + self.state.show_help = false; + } + KeyCode::Down | KeyCode::Char('j') => *scroll += 1, + KeyCode::Up | KeyCode::Char('k') => { + *scroll = scroll.saturating_sub(1); + } + KeyCode::PageDown => *scroll += HELP_PAGE, + KeyCode::PageUp => *scroll = scroll.saturating_sub(HELP_PAGE), + KeyCode::Home | KeyCode::Char('g') => *scroll = 0, + KeyCode::End | KeyCode::Char('G') => *scroll = usize::MAX, + _ => {} + } + return; + } + // Chat popup takes over navigation while open. if self.state.show_messages { return self.handle_chat_input(key); @@ -33,10 +56,10 @@ impl MainTui { return self.handle_settings_input(key); } + // Control combinations only ever page a list, so they never reach + // the letter keys below: ctrl-d is not d. if key.modifiers.contains(KeyModifiers::CONTROL) { - if self.state.focused_pane == FocusedPane::Results { - self.handle_results_input(key); - } + self.navigate_focused_list(key); return; } @@ -53,16 +76,41 @@ impl MainTui { self.state.should_exit = true; return; } + KeyCode::Char('?') => { + self.state.show_help = true; + self.state.help_scroll = 0; + return; + } KeyCode::Char('1') => { - self.state.focused_pane = FocusedPane::Searches; + self.state.focus_pane(FocusedPane::Searches); return; } KeyCode::Char('2') => { - self.state.focused_pane = FocusedPane::Results; + self.state.focus_pane(FocusedPane::Results); return; } KeyCode::Char('3') => { - self.state.focused_pane = FocusedPane::Downloads; + self.state.focus_pane(FocusedPane::Downloads); + return; + } + KeyCode::Tab => { + self.state.cycle_focus(true); + return; + } + KeyCode::BackTab => { + self.state.cycle_focus(false); + return; + } + KeyCode::Char('z') => { + self.state.toggle_zoom(); + return; + } + KeyCode::Char('w') => { + self.state.hide_focused_pane(); + return; + } + KeyCode::Esc if self.state.layout.zoomed => { + self.state.layout.zoomed = false; return; } KeyCode::Char('s') => { @@ -306,15 +354,45 @@ impl MainTui { } } + /// The list the focus is on and how far one page moves it. + fn navigate_focused_list(&mut self, key: KeyEvent) -> bool { + let focused = self.state.focused_pane; + let page = self.page_size(focused); + let (table, len) = match focused { + FocusedPane::Searches => ( + &mut self.state.searches_table_state, + self.state.searches.len(), + ), + FocusedPane::Results => { + let len = self.visible_results().len(); + (&mut self.state.results_table_state, len) + } + FocusedPane::Downloads => ( + &mut self.state.downloads_table_state, + self.state.downloads.len() + self.state.uploads.len(), + ), + }; + navigate_list(key, table, len, page) + } + + /// Rows a pane shows at once: its inner height less the table header. + fn page_size(&self, pane: FocusedPane) -> isize { + let area = match pane { + FocusedPane::Searches => self.state.searches_pane_area, + FocusedPane::Results => self.state.results_pane_area, + FocusedPane::Downloads => self.state.downloads_pane_area, + }; + let rows = area.map_or(1, |area| { + pane_block(false).inner(area).height.saturating_sub(1) + }); + isize::try_from(rows.max(1)).unwrap_or(isize::MAX) + } + fn handle_searches_input(&mut self, key: KeyEvent) { - let rows = self.state.searches.len(); + if self.navigate_focused_list(key) { + return; + } match key.code { - KeyCode::Up | KeyCode::Char('k') => { - cycle(&mut self.state.searches_table_state, rows, false); - } - KeyCode::Down | KeyCode::Char('j') => { - cycle(&mut self.state.searches_table_state, rows, true); - } KeyCode::Enter => { if let Some(selected) = self.state.searches_table_state.selected() @@ -329,7 +407,7 @@ impl MainTui { self.state.results_selected_indices.clear(); self.state.results_name_offset = 0; self.state.results_table_state.select(Some(0)); - self.state.focused_pane = FocusedPane::Results; + self.state.focus_pane(FocusedPane::Results); } } } @@ -348,31 +426,10 @@ impl MainTui { } fn handle_results_input(&mut self, key: KeyEvent) { - let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); - if ctrl && !matches!(key.code, KeyCode::Char('f' | 'b' | 'd' | 'u')) { + if self.navigate_focused_list(key) { return; } - let items_count = self.visible_results().len(); - let page = self.results_page_size(); - let half = (page / 2).max(1); - match key.code { - KeyCode::Up | KeyCode::Char('k') => { - cycle(&mut self.state.results_table_state, items_count, false); - } - KeyCode::Down | KeyCode::Char('j') => { - cycle(&mut self.state.results_table_state, items_count, true); - } - KeyCode::Home | KeyCode::Char('g') => self.select_row(0), - KeyCode::End | KeyCode::Char('G') => { - self.select_row(items_count.saturating_sub(1)); - } - KeyCode::PageDown => self.scroll_rows(page), - KeyCode::PageUp => self.scroll_rows(-page), - KeyCode::Char('f') if ctrl => self.scroll_rows(page), - KeyCode::Char('b') if ctrl => self.scroll_rows(-page), - KeyCode::Char('d') if ctrl => self.scroll_rows(half), - KeyCode::Char('u') if ctrl => self.scroll_rows(-half), KeyCode::Right | KeyCode::Char('l') => { self.scroll_names(NAME_SCROLL_STEP); } @@ -418,20 +475,6 @@ impl MainTui { } } - fn select_row(&mut self, target: usize) { - let len = self.visible_results().len(); - if len > 0 { - self.state - .results_table_state - .select(Some(target.min(len - 1))); - } - } - - fn scroll_rows(&mut self, delta: isize) { - let current = self.state.results_table_state.selected().unwrap_or(0); - self.select_row(current.saturating_add_signed(delta)); - } - fn scroll_names(&mut self, delta: isize) { let end = self.highlighted_name_end(); let current = self.state.results_name_offset.min(end); @@ -447,23 +490,12 @@ impl MainTui { .map_or(0, |file| name_end_offset(&file.filename, area)) } - fn results_page_size(&self) -> isize { - let rows = self.state.results_pane_area.map_or(1, |area| { - pane_block(false).inner(area).height.saturating_sub(1) - }); - rows.max(1) as isize - } - fn handle_downloads_input(&mut self, key: KeyEvent) { // The pane lists downloads first, then uploads; navigation spans both. - let rows = self.state.downloads.len() + self.state.uploads.len(); + if self.navigate_focused_list(key) { + return; + } match key.code { - KeyCode::Up | KeyCode::Char('k') => { - cycle(&mut self.state.downloads_table_state, rows, false); - } - KeyCode::Down | KeyCode::Char('j') => { - cycle(&mut self.state.downloads_table_state, rows, true); - } KeyCode::Char('x') => { self.cancel_selected_transfer(); } @@ -494,8 +526,12 @@ impl MainTui { (self.state.results_pane_area, FocusedPane::Results), (self.state.downloads_pane_area, FocusedPane::Downloads), ]; + // The areas are from the last draw. A pane hidden since then, in the + // same batch of events, still has one, and is not there to click. for (area, pane) in panes { - if area.is_some_and(|area| area.contains(clicked)) { + if area.is_some_and(|area| area.contains(clicked)) + && self.state.layout.is_visible(pane) + { self.state.focused_pane = pane; return; } @@ -503,6 +539,37 @@ impl MainTui { } } +/// The keys every list answers alike: a row at a time wrapping at either +/// end, a page or half a page at a time stopping there, and the two ends. +/// Says whether `key` was one of them. +fn navigate_list( + key: KeyEvent, + table: &mut TableState, + len: usize, + page: isize, +) -> bool { + let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); + let half = (page / 2).max(1); + match key.code { + KeyCode::Up | KeyCode::Char('k') if !ctrl => cycle(table, len, false), + KeyCode::Down | KeyCode::Char('j') if !ctrl => cycle(table, len, true), + KeyCode::Home | KeyCode::Char('g') if !ctrl => { + select_row(table, len, 0); + } + KeyCode::End | KeyCode::Char('G') if !ctrl => { + select_row(table, len, len.saturating_sub(1)); + } + KeyCode::PageDown => scroll_rows(table, len, page), + KeyCode::PageUp => scroll_rows(table, len, -page), + KeyCode::Char('f') if ctrl => scroll_rows(table, len, page), + KeyCode::Char('b') if ctrl => scroll_rows(table, len, -page), + KeyCode::Char('d') if ctrl => scroll_rows(table, len, half), + KeyCode::Char('u') if ctrl => scroll_rows(table, len, -half), + _ => return false, + } + true +} + /// Move a table's selection one row, wrapping at either end. fn cycle(table: &mut TableState, len: usize, forward: bool) { if len == 0 { @@ -516,3 +583,16 @@ fn cycle(table: &mut TableState, len: usize, forward: bool) { }; table.select(Some(next)); } + +/// Put the selection on `target`, or the last row when that is past the end. +fn select_row(table: &mut TableState, len: usize, target: usize) { + if len > 0 { + table.select(Some(target.min(len - 1))); + } +} + +/// Move the selection `delta` rows, stopping at either end. +fn scroll_rows(table: &mut TableState, len: usize, delta: isize) { + let current = table.selected().unwrap_or(0); + select_row(table, len, current.saturating_add_signed(delta)); +} diff --git a/soulseek-rs/src/ui/main_tui/mod.rs b/soulseek-rs/src/ui/main_tui/mod.rs index ea8389c..65de6ad 100644 --- a/soulseek-rs/src/ui/main_tui/mod.rs +++ b/soulseek-rs/src/ui/main_tui/mod.rs @@ -1341,13 +1341,320 @@ mod tests { assert!(tui.state.results_selected_indices.is_empty()); } - fn screen_of(tui: &mut MainTui) -> String { - let backend = ratatui::backend::TestBackend::new(160, 40); + fn screen_sized(tui: &mut MainTui, width: u16, height: u16) -> String { + let backend = ratatui::backend::TestBackend::new(width, height); let mut terminal = ratatui::Terminal::new(backend).expect("backend"); terminal.draw(|frame| tui.render(frame)).expect("draw"); terminal.backend().to_string() } + fn screen_of(tui: &mut MainTui) -> String { + screen_sized(tui, 160, 40) + } + + /// The screen row carrying a pane's legend, `[2] Results` say, without + /// the quotes the test backend wraps each row in. + fn row_with<'a>(screen: &'a str, needle: &str) -> &'a str { + let line = screen + .lines() + .find(|line| line.contains(needle)) + .unwrap_or_else(|| panic!("no row with {needle:?} in\n{screen}")); + let start = line.find('"').map_or(0, |i| i + 1); + let end = line.rfind('"').unwrap_or(line.len()); + &line[start..end] + } + + /// A window with a search, its results, and a transfer, so every pane + /// has something to show. + fn furnished_tui() -> MainTui { + let mut tui = results_tui(3); + tui.state.searches.push(crate::models::SearchEntry { + query: "aphex twin".to_string(), + status: SearchStatus::Completed, + results: results(3), + known_files: 3, + owned: true, + start_time: std::time::Instant::now(), + cancel_flag: Arc::new(std::sync::atomic::AtomicBool::new(false)), + }); + tui.state.selected_search_index = Some(0); + tui.state.downloads.push(crate::models::DownloadEntry { + download: queued("bob", "@@x\\Music\\00.mp3"), + receiver: None, + }); + tui + } + + #[test] + fn the_results_pane_has_the_whole_width_to_itself() { + let mut tui = furnished_tui(); + let screen = screen_of(&mut tui); + + let results = row_with(&screen, "[2] Results"); + assert!(results.starts_with('╭'), "{results}"); + assert!(results.trim_end().ends_with('╮'), "{results}"); + assert!(!results.contains("[1]"), "nothing beside it: {results}"); + + // The other three share the row underneath, in this order. + let row = row_with(&screen, "[1] Searches"); + let searches = row.find("[1]").expect("searches"); + let downloads = row.find("[3]").expect("downloads"); + let info = row.find("[Info]").expect("info"); + assert!(searches < downloads && downloads < info, "{row}"); + } + + #[test] + fn tab_and_shift_tab_walk_the_panes_in_legend_order() { + let mut tui = furnished_tui(); + tui.state.focused_pane = FocusedPane::Searches; + press(&mut tui, KeyCode::Tab); + assert_eq!(tui.state.focused_pane, FocusedPane::Results); + press(&mut tui, KeyCode::Tab); + assert_eq!(tui.state.focused_pane, FocusedPane::Downloads); + press(&mut tui, KeyCode::Tab); + assert_eq!(tui.state.focused_pane, FocusedPane::Searches); + press(&mut tui, KeyCode::BackTab); + assert_eq!(tui.state.focused_pane, FocusedPane::Downloads); + } + + #[test] + fn w_hides_the_focused_pane_and_its_number_brings_it_back() { + let mut tui = furnished_tui(); + tui.state.focused_pane = FocusedPane::Searches; + + press(&mut tui, KeyCode::Char('w')); + + let screen = screen_of(&mut tui); + assert!(!screen.contains("[1] Searches"), "{screen}"); + assert!(screen.contains("[2] Results"), "{screen}"); + assert!(screen.contains("[3] Downloads"), "{screen}"); + assert_eq!(tui.state.focused_pane, FocusedPane::Results); + assert_eq!(tui.state.searches_pane_area, None, "nothing to click"); + // The transfers widen into the space. + let row = row_with(&screen, "[3] Downloads"); + assert!(row.starts_with('╭'), "{row}"); + + press(&mut tui, KeyCode::Char('1')); + + let screen = screen_of(&mut tui); + assert!(screen.contains("[1] Searches"), "{screen}"); + assert_eq!(tui.state.focused_pane, FocusedPane::Searches); + } + + #[test] + fn hiding_the_results_gives_the_row_the_whole_height() { + let mut tui = furnished_tui(); + press(&mut tui, KeyCode::Char('w')); + let screen = screen_of(&mut tui); + assert!(!screen.contains("[2] Results"), "{screen}"); + // The row now starts right under the status bar. + let status = screen + .lines() + .position(|line| line.contains("Status")) + .expect("status bar"); + let row = screen + .lines() + .position(|line| line.contains("[1] Searches")) + .expect("searches"); + assert_eq!(row, status + 3, "{screen}"); + } + + #[test] + fn the_last_pane_standing_cannot_be_hidden() { + // Results goes, then Downloads; Searches is what is left, and a + // third press leaves it there. + let mut tui = furnished_tui(); + press(&mut tui, KeyCode::Char('w')); + press(&mut tui, KeyCode::Char('w')); + press(&mut tui, KeyCode::Char('w')); + let screen = screen_of(&mut tui); + assert!(screen.contains("[1] Searches"), "{screen}"); + assert!(!screen.contains("[3] Downloads"), "{screen}"); + assert_eq!(tui.state.focused_pane, FocusedPane::Searches); + } + + #[test] + fn z_zooms_the_focused_pane_and_esc_or_z_leaves_it() { + let mut tui = furnished_tui(); + press(&mut tui, KeyCode::Char('z')); + + let screen = screen_of(&mut tui); + assert!(screen.contains("[2] Results"), "{screen}"); + assert!(!screen.contains("[1] Searches"), "{screen}"); + assert!(!screen.contains("[3] Downloads"), "{screen}"); + assert!(!screen.contains("[Info]"), "{screen}"); + assert!(screen.contains("[z → unzoom]"), "{screen}"); + + // Tab while zoomed switches which pane fills the window. + press(&mut tui, KeyCode::Tab); + let screen = screen_of(&mut tui); + assert!(screen.contains("[3] Downloads"), "{screen}"); + assert!(!screen.contains("[2] Results"), "{screen}"); + + press(&mut tui, KeyCode::Esc); + assert!(!tui.state.layout.zoomed); + let screen = screen_of(&mut tui); + assert!(screen.contains("[2] Results"), "{screen}"); + assert!(screen.contains("[Info]"), "{screen}"); + + press(&mut tui, KeyCode::Char('z')); + press(&mut tui, KeyCode::Char('z')); + assert!(!tui.state.layout.zoomed, "z toggles"); + } + + #[test] + fn a_search_brings_a_hidden_results_pane_back() { + let mut tui = furnished_tui(); + press(&mut tui, KeyCode::Char('w')); + assert!(!tui.state.layout.is_visible(FocusedPane::Results)); + + press(&mut tui, KeyCode::Char('s')); + press(&mut tui, KeyCode::Char('x')); + press(&mut tui, KeyCode::Enter); + + assert!(tui.state.layout.is_visible(FocusedPane::Results)); + assert_eq!(tui.state.focused_pane, FocusedPane::Results); + } + + #[test] + fn question_mark_opens_the_keys_list_and_closes_it_again() { + let mut tui = furnished_tui(); + press(&mut tui, KeyCode::Char('?')); + + let screen = screen_of(&mut tui); + assert!(screen.contains("Keys"), "{screen}"); + assert!(screen.contains("Tab / Shift-Tab"), "{screen}"); + assert!(screen.contains("clear every finished one"), "{screen}"); + + // Other keys are swallowed while it is open. + press(&mut tui, KeyCode::Char('q')); + assert!(!tui.state.should_exit, "q closes the list, not the app"); + assert!(!tui.state.show_help); + + press(&mut tui, KeyCode::Char('?')); + press(&mut tui, KeyCode::Char('w')); + assert!(tui.state.show_help, "w did not reach the panes"); + assert!(tui.state.layout.is_visible(FocusedPane::Results)); + press(&mut tui, KeyCode::Esc); + assert!(!tui.state.show_help); + } + + #[test] + fn on_a_small_terminal_the_keys_list_stacks_and_scrolls() { + let mut tui = furnished_tui(); + press(&mut tui, KeyCode::Char('?')); + + let screen = screen_sized(&mut tui, 80, 24); + let panes = row_with(&screen, "Panes"); + assert!(!panes.contains("Searches"), "one column: {panes}"); + assert!(screen.contains("focus a pane, hidden or not"), "{screen}"); + assert!(!screen.contains("clear every finished one"), "{screen}"); + + press(&mut tui, KeyCode::End); + let screen = screen_sized(&mut tui, 80, 24); + assert!(screen.contains("clear every finished one"), "{screen}"); + assert!(!screen.contains("Tab / Shift-Tab"), "{screen}"); + + press(&mut tui, KeyCode::Home); + let screen = screen_sized(&mut tui, 80, 24); + assert!(screen.contains("Tab / Shift-Tab"), "{screen}"); + + // A wide window shows both columns at once, nothing to scroll. + press(&mut tui, KeyCode::End); + let screen = screen_of(&mut tui); + let panes = row_with(&screen, "Panes"); + assert!(panes.contains("Searches"), "two columns: {panes}"); + assert!(screen.contains("Tab / Shift-Tab"), "{screen}"); + } + + #[test] + fn a_click_where_a_pane_just_hid_does_not_focus_it() { + let mut tui = furnished_tui(); + let _ = screen_of(&mut tui); + let searches = tui.state.searches_pane_area.expect("laid out"); + // Hidden, but not yet redrawn: the old area is still on record. + tui.state.focused_pane = FocusedPane::Searches; + press(&mut tui, KeyCode::Char('w')); + assert_eq!(tui.state.focused_pane, FocusedPane::Results); + tui.handle_mouse_event(ratatui::crossterm::event::MouseEvent { + kind: ratatui::crossterm::event::MouseEventKind::Down( + ratatui::crossterm::event::MouseButton::Left, + ), + column: searches.x + 1, + row: searches.y + 1, + modifiers: KeyModifiers::NONE, + }); + assert_eq!(tui.state.focused_pane, FocusedPane::Results); + } + + #[test] + fn the_shortcut_bar_wraps_instead_of_cutting_keys_off() { + let mut tui = furnished_tui(); + let screen = screen_of(&mut tui); + assert!(screen.contains("[q → quit]"), "{screen}"); + let bar_rows = + screen.lines().filter(|line| line.contains(" → ")).count(); + assert_eq!(bar_rows, 2, "{screen}"); + + let wide = screen_sized(&mut tui, 300, 40); + let bar_rows = wide.lines().filter(|line| line.contains(" → ")).count(); + assert_eq!(bar_rows, 1, "{wide}"); + } + + #[test] + fn page_keys_work_in_the_transfers_and_searches_lists_too() { + let mut tui = furnished_tui(); + for i in 0..30 { + tui.state.downloads.push(crate::models::DownloadEntry { + download: queued("bob", &format!("{i}.mp3")), + receiver: None, + }); + } + tui.state.focused_pane = FocusedPane::Downloads; + tui.state.downloads_table_state.select(Some(0)); + let _ = screen_of(&mut tui); // lays the panes out, which sets the page size + + press(&mut tui, KeyCode::End); + assert_eq!(tui.state.downloads_table_state.selected(), Some(30)); + press(&mut tui, KeyCode::Home); + assert_eq!(tui.state.downloads_table_state.selected(), Some(0)); + press(&mut tui, KeyCode::PageDown); + let page = tui.state.downloads_table_state.selected().expect("row"); + assert!(page > 1 && page < 30, "a pane of rows, not one: {page}"); + ctrl(&mut tui, 'd'); + assert_eq!(tui.state.downloads.len(), 31, "ctrl-d pages, d deletes"); + assert!(tui.state.downloads_table_state.selected() > Some(page)); + + tui.state.focused_pane = FocusedPane::Searches; + press(&mut tui, KeyCode::Char('G')); + assert_eq!(tui.state.searches_table_state.selected(), Some(0)); + press(&mut tui, KeyCode::Char('g')); + assert_eq!(tui.state.searches_table_state.selected(), Some(0)); + assert_eq!(tui.state.searches.len(), 1, "g and G only move"); + } + + #[test] + fn clicking_where_a_hidden_pane_was_focuses_whatever_is_there_now() { + let mut tui = furnished_tui(); + let _ = screen_of(&mut tui); + let searches = tui.state.searches_pane_area.expect("laid out"); + let inside = ratatui::crossterm::event::MouseEvent { + kind: ratatui::crossterm::event::MouseEventKind::Down( + ratatui::crossterm::event::MouseButton::Left, + ), + column: searches.x + 1, + row: searches.y + 1, + modifiers: KeyModifiers::NONE, + }; + tui.handle_mouse_event(inside); + assert_eq!(tui.state.focused_pane, FocusedPane::Searches); + + press(&mut tui, KeyCode::Char('w')); + let _ = screen_of(&mut tui); + tui.handle_mouse_event(inside); + assert_eq!(tui.state.focused_pane, FocusedPane::Downloads); + } + #[test] fn the_info_pane_describes_the_highlighted_result() { let mut tui = results_tui(3); diff --git a/soulseek-rs/src/ui/main_tui/render.rs b/soulseek-rs/src/ui/main_tui/render.rs index c4abbfb..06f7b9c 100644 --- a/soulseek-rs/src/ui/main_tui/render.rs +++ b/soulseek-rs/src/ui/main_tui/render.rs @@ -6,19 +6,92 @@ use crate::ui::panes::{ render_rooms_pane, render_searches_pane, selected_transfer, }; use crate::ui::{ - accent_style, dimmed_style, format_shortcuts_styled, pane_block, + SIGNAL, accent_style, body_style, dimmed_style, pack_shortcuts, pane_block, plain_title, primary_style, render_download_stats, warning_style, }; use ratatui::{ Frame, layout::{Alignment, Constraint, Layout, Position, Rect}, - style::Modifier, + style::{Modifier, Style}, text::{Line, Span}, widgets::Paragraph, }; +/// The keys overlay, grouped by where a key applies. Two columns, because a +/// single list runs longer than most terminals are tall. +const HELP_LEFT: &[(&str, &[(&str, &str)])] = &[ + ( + "Panes", + &[ + ("Tab / Shift-Tab", "next / previous pane"), + ("1 2 3", "focus a pane, hidden or not"), + ("z", "zoom the focused pane"), + ("w", "hide the focused pane"), + ("Esc", "leave zoom"), + ("click", "focus a pane"), + ], + ), + ( + "Anywhere", + &[ + ("s", "search the network"), + ("b", "browse a user's files"), + ("m", "compose a private message"), + ("i", "inbox"), + ("o", "settings"), + ("?", "this list"), + ("q", "quit"), + ], + ), + ( + "Any list", + &[ + ("↑ ↓ / k j", "move one row"), + ("Home End / g G", "first / last row"), + ("PgUp PgDn / ^b ^f", "one page"), + ("^u ^d", "half a page"), + ], + ), +]; + +const HELP_RIGHT: &[(&str, &[(&str, &str)])] = &[ + ( + "Searches", + &[ + ("Enter", "show its results"), + ("d", "remove the search"), + ("C", "clear every search"), + ("c", "chat rooms"), + ], + ), + ( + "Results", + &[ + ("Space", "select / deselect"), + ("a / A", "select all / none"), + ("Enter", "download the selection"), + ("/", "filter, Enter keeps it, Esc clears"), + ("h l / ← →", "scroll the file name"), + ("0 / $", "start / end of the file name"), + ("b", "browse the owner"), + ("c", "chat rooms"), + ], + ), + ( + "Downloads / Uploads", + &[ + ("p", "pause / resume"), + ("x", "cancel"), + ("r", "retry a failed one"), + ("d", "delete a queued or finished one"), + ("c", "clear every finished one"), + ], + ), +]; + impl MainTui { pub(super) fn render(&mut self, frame: &mut Frame) { + let shortcuts = self.shortcut_rows(frame.area().width); let mut constraints = vec![ Constraint::Length(3), // Status bar Constraint::Fill(1), // Main content @@ -26,7 +99,7 @@ impl MainTui { if self.state.command_bar_active { constraints.push(Constraint::Length(3)); // Command bar } - constraints.push(Constraint::Length(3)); // Shortcuts + constraints.push(Constraint::Length(shortcuts.len() as u16 + 2)); let main_chunks = Layout::vertical(constraints).split(frame.area()); let daemon = self.client.daemon_endpoint(); @@ -43,55 +116,111 @@ impl MainTui { // Render command bar if active (vim-style, above shortcuts) if self.state.command_bar_active { self.render_command_bar(frame, main_chunks[2]); - self.render_shortcuts(frame, main_chunks[3]); + self.render_shortcuts(frame, main_chunks[3], shortcuts); } else { - self.render_shortcuts(frame, main_chunks[2]); + self.render_shortcuts(frame, main_chunks[2], shortcuts); } self.render_overlays(frame); } + /// Results across the top, where a long file name has the whole width; + /// Searches, Downloads and Info in a row underneath. A hidden pane drops + /// out of its row and the rest widen; a zoomed one has the area alone. fn render_content(&mut self, frame: &mut Frame, area: Rect) { - // Split main content area - let content_chunks = Layout::horizontal([ - Constraint::Percentage(30), // Searches pane - Constraint::Percentage(70), // Results + Downloads - ]) - .split(area); + // Only what is drawn can be clicked. + self.state.searches_pane_area = None; + self.state.results_pane_area = None; + self.state.downloads_pane_area = None; - // Store searches pane area - self.state.searches_pane_area = Some(content_chunks[0]); + if self.state.layout.zoomed { + self.render_pane(frame, area, self.state.focused_pane); + return; + } - // Render Searches pane (left) - render_searches_pane( - frame, - content_chunks[0], - &self.state.searches, - &mut self.state.searches_table_state, - self.state.focused_pane == FocusedPane::Searches, - ); + let row_panes: Vec = + [FocusedPane::Searches, FocusedPane::Downloads] + .into_iter() + .filter(|pane| self.state.layout.is_visible(*pane)) + .collect(); - // Split right side into Results (top) and Downloads (bottom) - let right_chunks = Layout::vertical([ - Constraint::Percentage(60), // Results - Constraint::Percentage(40), // Downloads - ]) - .split(content_chunks[1]); + let row_area = if self.state.layout.is_visible(FocusedPane::Results) { + // With only Info left in the row, the results deserve more of + // the height. + let row_weight = if row_panes.is_empty() { 1 } else { 2 }; + let [top, bottom] = Layout::vertical([ + Constraint::Fill(3), + Constraint::Fill(row_weight), + ]) + .areas(area); + self.render_pane(frame, top, FocusedPane::Results); + bottom + } else { + area + }; - // Split bottom-right into Downloads list and Info pane - let downloads_chunks = Layout::horizontal([ - Constraint::Percentage(60), // Downloads list - Constraint::Percentage(40), // Info pane - ]) - .split(right_chunks[1]); + // Transfers carry the most columns, so they get the most width. + let mut constraints: Vec = row_panes + .iter() + .map(|pane| match pane { + FocusedPane::Searches => Constraint::Fill(5), + FocusedPane::Downloads => Constraint::Fill(10), + FocusedPane::Results => unreachable!("not a row pane"), + }) + .collect(); + constraints.push(Constraint::Fill(6)); // Info + let chunks = Layout::horizontal(constraints).split(row_area); + for (chunk, pane) in chunks.iter().zip(&row_panes) { + self.render_pane(frame, *chunk, *pane); + } + self.render_info_pane(frame, chunks[row_panes.len()]); + } - // Store results and downloads pane areas - self.state.results_pane_area = Some(right_chunks[0]); - self.state.downloads_pane_area = Some(downloads_chunks[0]); + fn render_pane( + &mut self, + frame: &mut Frame, + area: Rect, + pane: FocusedPane, + ) { + let focused = self.state.focused_pane == pane; + match pane { + FocusedPane::Searches => { + self.state.searches_pane_area = Some(area); + render_searches_pane( + frame, + area, + &self.state.searches, + &mut self.state.searches_table_state, + focused, + ); + } + FocusedPane::Results => { + self.state.results_pane_area = Some(area); + self.render_results_pane(frame, area, focused); + } + FocusedPane::Downloads => { + self.state.downloads_pane_area = Some(area); + render_downloads_pane( + frame, + area, + &self.state.downloads, + &self.state.uploads, + &mut self.state.downloads_table_state, + focused, + ); + } + } + } - // Render Results pane. When a filter is active the rendered rows are a - // subset, so the pane also needs the mapping back to unfiltered indices - // to render the selection checkboxes correctly. + fn render_results_pane( + &mut self, + frame: &mut Frame, + area: Rect, + focused: bool, + ) { + // When a filter is active the rendered rows are a subset, so the pane + // also needs the mapping back to unfiltered indices to render the + // selection checkboxes correctly. let (results_items, results_original_indices) = if self.state.results_filter_query.is_empty() { (&self.state.results_items, None) @@ -110,7 +239,7 @@ impl MainTui { render_results_pane( frame, - right_chunks[0], + area, ResultsPaneParams { items: results_items, table_state: &mut self.state.results_table_state, @@ -118,21 +247,16 @@ impl MainTui { original_indices: results_original_indices, filter_query: &self.state.results_filter_query, is_filtering: self.state.results_is_filtering, - focused: self.state.focused_pane == FocusedPane::Results, + focused, active_search_query, name_offset: self.state.results_name_offset, }, ); + } - render_downloads_pane( - frame, - downloads_chunks[0], - &self.state.downloads, - &self.state.uploads, - &mut self.state.downloads_table_state, - self.state.focused_pane == FocusedPane::Downloads, - ); - + /// The Info pane follows the focus: the highlighted result, or the + /// highlighted transfer. + fn render_info_pane(&self, frame: &mut Frame, area: Rect) { let selected = if self.state.focused_pane == FocusedPane::Results { self.highlighted_result().map(InfoSubject::Result) } else { @@ -144,7 +268,7 @@ impl MainTui { }; render_download_info_pane( frame, - downloads_chunks[1], + area, selected, matches!( self.state.focused_pane, @@ -153,6 +277,49 @@ impl MainTui { ); } + /// The keys list, two columns side by side where the terminal is wide + /// enough and one on top of the other where it is not, scrolling when + /// even that is taller than the window. + fn render_help_popup(&mut self, frame: &mut Frame) { + let screen = frame.area(); + // Borders and padding on either side of the text. + let frame_width = 4; + let width = screen.width.min(help_width() + frame_width); + let columns = help_columns(width.saturating_sub(frame_width)); + let tallest = columns.iter().map(Vec::len).max().unwrap_or(0); + let height = screen + .height + .min(u16::try_from(tallest + 2).unwrap_or(u16::MAX)); + let area = Rect::new( + screen.x + (screen.width - width) / 2, + screen.y + (screen.height - height) / 2, + width, + height, + ); + frame.render_widget(ratatui::widgets::Clear, area); + + let block = pane_block(true).title(plain_title("Keys", true)); + let inner = block.inner(area); + frame.render_widget(block, area); + + // Both columns scroll together, and no further than the taller one + // needs; the clamp is written back so the keys cannot run past it. + let furthest = tallest.saturating_sub(usize::from(inner.height)); + self.state.help_scroll = self.state.help_scroll.min(furthest); + let scroll = u16::try_from(self.state.help_scroll).unwrap_or(u16::MAX); + + let areas = + Layout::horizontal(vec![Constraint::Fill(1); columns.len()]) + .spacing(HELP_GAP) + .split(inner); + for (area, lines) in areas.iter().zip(columns) { + frame.render_widget( + Paragraph::new(lines).scroll((scroll, 0)), + *area, + ); + } + } + fn render_overlays(&mut self, frame: &mut Frame) { // Messages inbox overlays everything when open. if self.state.show_messages { @@ -188,6 +355,12 @@ impl MainTui { &mut self.state.rooms_list_table_state, ); } + + // The keys list only opens from the main view, so it is on top of + // nothing; drawn last so that stays true if that ever changes. + if self.state.show_help { + self.render_help_popup(frame); + } } fn render_settings_popup(&self, frame: &mut Frame) { @@ -304,10 +477,31 @@ impl MainTui { } } + /// The keys every pane shares for moving between and resizing panes, + /// ending the bar the same way wherever the focus sits. + fn pane_shortcuts(&self) -> Vec<(&'static str, &'static str)> { + vec![ + ("Tab/1-3", "pane"), + ( + "z", + if self.state.layout.zoomed { + "unzoom" + } else { + "zoom" + }, + ), + ("w", "hide"), + ("?", "keys"), + ("q", "quit"), + ] + } + /// Which shortcuts the bar offers, which is purely a question of what is /// open and where the focus sits. Sibling of [`Self::rooms_shortcuts`]. fn shortcuts(&self) -> Vec<(&'static str, &'static str)> { - if self.state.settings.is_some() { + if self.state.show_help { + vec![("?/Esc", "close")] + } else if self.state.settings.is_some() { vec![ ("↑↓", "move"), ("Enter/e", "edit download dir"), @@ -364,29 +558,28 @@ impl MainTui { ("Esc", "cancel"), ], } + } else if self.state.results_is_filtering + && self.state.focused_pane == FocusedPane::Results + { + // Typing goes to the filter, so no letter keys are on offer here. + vec![ + ("Type", "filter"), + ("↑/↓", "navigate"), + ("PgUp/PgDn", "page"), + ("Enter", "keep filter"), + ("Esc", "clear filter"), + ] } else { - match self.state.focused_pane { + let mut keys = match self.state.focused_pane { FocusedPane::Searches => vec![ ("s", "search"), + ("Enter", "results"), + ("d", "remove"), ("m", "message"), ("i", "inbox"), ("c", "chat"), ("b", "browse user"), - ("1-3", "focus pane"), - ("↑↓", "navigate"), - ("Enter", "results"), - ("q", "quit"), ], - FocusedPane::Results if self.state.results_is_filtering => { - vec![ - ("Type", "filter"), - ("↑/↓", "navigate"), - ("Enter", "confirm"), - ("Esc", "clear filter"), - ("1-3", "focus pane"), - ("q", "quit"), - ] - } FocusedPane::Results => vec![ ("Space", "select"), ("Enter", "download"), @@ -394,29 +587,50 @@ impl MainTui { ("c", "chat"), ("/", "filter"), ("a/A", "select all/none"), - ("h/l", "scroll"), + ("h/l", "scroll name"), ("g/G", "top/end"), - ("1-3", "focus pane"), - ("q", "quit"), ], - FocusedPane::Downloads => { - vec![ - ("p", "pause/resume"), - ("x", "cancel"), - ("r", "retry failed"), - ("d", "delete queued/done"), - ("c", "clear finished"), - ("b", "browse user"), - ("1-3", "focus pane"), - ("q", "quit"), - ] - } - } + FocusedPane::Downloads => vec![ + ("p", "pause/resume"), + ("x", "cancel"), + ("r", "retry failed"), + ("d", "delete queued/done"), + ("c", "clear finished"), + ("b", "browse user"), + ], + }; + keys.extend(self.pane_shortcuts()); + keys } } - fn render_shortcuts(&self, frame: &mut Frame, area: Rect) { - let shortcuts_line = format_shortcuts_styled(&self.shortcuts()); + /// The legend packed into rows for a window `width` wide, so a narrow + /// terminal grows the bar instead of cutting keys off its right edge. + fn shortcut_rows(&self, width: u16) -> Vec> { + // Borders and padding, and the unread badge's column when it shows. + let badge = self.unread_badge().width() as u16; + let reserved = 4 + if badge > 0 { badge + 1 } else { 0 }; + pack_shortcuts(&self.shortcuts(), width.saturating_sub(reserved)) + } + + /// A prominent unread badge for the shortcuts bar. It rides in the + /// always-visible bar, so unread mail/chat shows in every pane, not just + /// some. The spinner tick (0..10, ~100ms each) drives a ~1s blink: bright + /// for the first half of the cycle, dim for the second. + fn unread_badge(&self) -> Line<'static> { + unread_indicator( + self.state.unread_messages, + self.state.rooms.total_unread(), + self.spinner_state < 5, + ) + } + + fn render_shortcuts( + &self, + frame: &mut Frame, + area: Rect, + rows: Vec>, + ) { // Surface our own sharing status in the block title. let shared = self.client.shared_directories(); let sharing = match shared.as_slice() { @@ -431,26 +645,20 @@ impl MainTui { let inner = block.inner(area); frame.render_widget(block, area); - // A prominent unread badge, right-aligned so the context shortcuts on - // the left never crowd it out. It rides in the always-visible shortcuts - // bar, so unread mail/chat shows in every pane, not just some. The - // spinner tick (0..10, ~100ms each) drives a ~1s blink: bright for the - // first half of the cycle, dim for the second. - let unread = unread_indicator( - self.state.unread_messages, - self.state.rooms.total_unread(), - self.spinner_state < 5, - ); + // The unread badge is right-aligned so the context shortcuts on the + // left never crowd it out. + let unread = self.unread_badge(); if unread.spans.is_empty() { - frame.render_widget(Paragraph::new(shortcuts_line), inner); + frame.render_widget(Paragraph::new(rows), inner); } else { let badge_width = unread.width() as u16; let cols = Layout::horizontal([ Constraint::Fill(1), Constraint::Length(badge_width), ]) + .spacing(1) .split(inner); - frame.render_widget(Paragraph::new(shortcuts_line), cols[0]); + frame.render_widget(Paragraph::new(rows), cols[0]); frame.render_widget( Paragraph::new(unread).alignment(Alignment::Right), cols[1], @@ -493,6 +701,72 @@ impl MainTui { } } +/// Columns between the two halves of the keys list. +const HELP_GAP: u16 = 2; + +/// The widest key in a column, so the descriptions line up. +fn key_column_width(sections: &[(&str, &[(&str, &str)])]) -> usize { + sections + .iter() + .flat_map(|(_, keys)| keys.iter()) + .map(|(key, _)| Span::raw(*key).width()) + .max() + .unwrap_or(0) +} + +/// Cells a column of the keys list needs to show every line whole. +fn help_column_width(sections: &[(&str, &[(&str, &str)])]) -> u16 { + let widest = help_lines(sections) + .iter() + .map(Line::width) + .max() + .unwrap_or(0); + u16::try_from(widest).unwrap_or(u16::MAX) +} + +/// The text width of the keys list with both halves side by side. +fn help_width() -> u16 { + help_column_width(HELP_LEFT) + HELP_GAP + help_column_width(HELP_RIGHT) +} + +/// The keys list laid out for `width` cells of text: two columns when both +/// fit beside each other, otherwise the right half under the left. +fn help_columns(width: u16) -> Vec>> { + let left = help_lines(HELP_LEFT); + let right = help_lines(HELP_RIGHT); + if width >= help_width() { + return vec![left, right]; + } + let mut stacked = left; + stacked.push(Line::from("")); + stacked.extend(right); + vec![stacked] +} + +fn help_lines(sections: &[(&str, &[(&str, &str)])]) -> Vec> { + let key_width = key_column_width(sections); + let mut lines = Vec::new(); + for (index, (heading, keys)) in sections.iter().enumerate() { + if index > 0 { + lines.push(Line::from("")); + } + lines.push(Line::styled( + (*heading).to_string(), + accent_style().add_modifier(Modifier::BOLD), + )); + for (key, action) in *keys { + lines.push(Line::from(vec![ + Span::styled( + format!("{key: Rect { let vertical = Layout::vertical([ diff --git a/soulseek-rs/src/ui/main_tui/search.rs b/soulseek-rs/src/ui/main_tui/search.rs index bea4d0f..d574d11 100644 --- a/soulseek-rs/src/ui/main_tui/search.rs +++ b/soulseek-rs/src/ui/main_tui/search.rs @@ -231,8 +231,8 @@ impl MainTui { self.state.results_table_state.select(Some(0)); self.state.results_name_offset = 0; - // Switch focus to Results pane - self.state.focused_pane = FocusedPane::Results; + // Switch focus to Results pane, bringing it back if it was hidden + self.state.focus_pane(FocusedPane::Results); let client = self.client.clone(); let timeout = self.search_timeout; diff --git a/soulseek-rs/src/ui/panes/downloads_pane.rs b/soulseek-rs/src/ui/panes/downloads_pane.rs index 493b6e8..2684fda 100644 --- a/soulseek-rs/src/ui/panes/downloads_pane.rs +++ b/soulseek-rs/src/ui/panes/downloads_pane.rs @@ -163,12 +163,15 @@ pub fn render_downloads_pane( ]) })); + // Sized to the widest value each column shows in ordinary use — a + // three-digit megabyte count on both sides of the progress — so the file + // name keeps whatever width the pane has beyond that. let widths = [ - ratatui::layout::Constraint::Length(8), - ratatui::layout::Constraint::Fill(2), - ratatui::layout::Constraint::Length(15), - ratatui::layout::Constraint::Length(25), + ratatui::layout::Constraint::Length(6), + ratatui::layout::Constraint::Fill(1), ratatui::layout::Constraint::Length(12), + ratatui::layout::Constraint::Length(25), + ratatui::layout::Constraint::Length(10), ]; let table = Table::new(rows, widths) diff --git a/soulseek-rs/src/ui/panes/searches_pane.rs b/soulseek-rs/src/ui/panes/searches_pane.rs index a0aaeaf..8942d83 100644 --- a/soulseek-rs/src/ui/panes/searches_pane.rs +++ b/soulseek-rs/src/ui/panes/searches_pane.rs @@ -43,10 +43,12 @@ pub fn render_searches_pane( } else { search.results.len() }; + // Nothing yet from a search still out: the status column already + // says it is looking. let results_text = if results_count == 0 && search.status == SearchStatus::Active { - "Searching...".to_string() + "…".to_string() } else { format!("{results_count}") }; @@ -59,10 +61,12 @@ pub fn render_searches_pane( }) .collect(); + // The pane shares a row with two others, so the fixed columns are as + // narrow as their widest word: the query gets the rest. let widths = [ - ratatui::layout::Constraint::Length(15), + ratatui::layout::Constraint::Length(6), ratatui::layout::Constraint::Fill(1), - ratatui::layout::Constraint::Length(10), + ratatui::layout::Constraint::Length(7), ]; let table = Table::new(rows, widths) diff --git a/soulseek-rs/src/ui/styles.rs b/soulseek-rs/src/ui/styles.rs index 2df9ab5..930cc02 100644 --- a/soulseek-rs/src/ui/styles.rs +++ b/soulseek-rs/src/ui/styles.rs @@ -158,22 +158,59 @@ pub fn download_status_glyph(status: &DownloadStatus) -> (&'static str, Style) { } } -pub fn format_shortcuts_styled(shortcuts: &[(&str, &str)]) -> Line<'static> { - let mut spans = Vec::new(); +/// One `[key → action]` legend entry. +fn shortcut_spans(key: &str, action: &str) -> Vec> { + vec![ + Span::styled("[", dimmed_style()), + Span::styled(key.to_string(), Style::default().fg(SIGNAL)), + Span::styled(format!(" {SHORTCUT_ARROW} "), dimmed_style()), + Span::styled(action.to_string(), body_style()), + Span::styled("]", dimmed_style()), + ] +} - for (i, (key, action)) in shortcuts.iter().enumerate() { - if i > 0 { - spans.push(Span::raw(" ")); +/// The most rows the shortcuts bar grows to. Past that a terminal is too +/// narrow for the legend to be worth the content rows it would take. +pub const SHORTCUT_ROWS_MAX: usize = 3; + +/// Lay the legend out in rows no wider than `width`, never breaking inside an +/// entry: a `[key → action]` split over two rows reads as two keys. Entries +/// that do not fit in [`SHORTCUT_ROWS_MAX`] rows are dropped, so a bar always +/// has at least one row and never more than that. +pub fn pack_shortcuts( + shortcuts: &[(&str, &str)], + width: u16, +) -> Vec> { + let width = usize::from(width); + let mut rows: Vec> = Vec::new(); + let mut row: Vec> = Vec::new(); + let mut row_width = 0; + + for (key, action) in shortcuts { + let entry = shortcut_spans(key, action); + let entry_width: usize = entry.iter().map(Span::width).sum(); + let needed = if row.is_empty() { + entry_width + } else { + row_width + 1 + entry_width + }; + if !row.is_empty() && needed > width { + rows.push(Line::from(std::mem::take(&mut row))); + if rows.len() == SHORTCUT_ROWS_MAX { + return rows; + } + row_width = entry_width; + } else { + if !row.is_empty() { + row.push(Span::raw(" ")); + } + row_width = needed; } - - spans.push(Span::styled("[", dimmed_style())); - spans.push(Span::styled(key.to_string(), Style::default().fg(SIGNAL))); - spans.push(Span::styled(format!(" {SHORTCUT_ARROW} "), dimmed_style())); - spans.push(Span::styled(action.to_string(), body_style())); - spans.push(Span::styled("]", dimmed_style())); + row.extend(entry); } - Line::from(spans) + rows.push(Line::from(row)); + rows } pub fn format_progress_bar( @@ -253,6 +290,46 @@ mod tests { assert_eq!(cursor.bg, Some(SELECTION)); } + fn texts(rows: &[Line<'static>]) -> Vec { + rows.iter() + .map(|line| line.spans.iter().map(|s| s.content.as_ref()).collect()) + .collect() + } + + #[test] + fn shortcuts_stay_on_one_row_when_they_fit() { + let rows = pack_shortcuts(&[("q", "quit"), ("s", "search")], 80); + assert_eq!(texts(&rows), ["[q → quit] [s → search]"]); + } + + #[test] + fn shortcuts_wrap_between_entries_never_inside_one() { + let rows = pack_shortcuts( + &[("Space", "select"), ("Enter", "download"), ("q", "quit")], + 30, + ); + // "[Space → select] [Enter → download]" is 35 cells, over the 30 the + // bar has, so the second entry starts a new row whole. + assert_eq!( + texts(&rows), + ["[Space → select]", "[Enter → download] [q → quit]"] + ); + } + + #[test] + fn an_entry_wider_than_the_bar_still_gets_a_row() { + let rows = pack_shortcuts(&[("Enter", "download"), ("q", "quit")], 5); + assert_eq!(texts(&rows), ["[Enter → download]", "[q → quit]"]); + } + + #[test] + fn the_bar_never_grows_past_its_cap() { + let keys: Vec<(&str, &str)> = (0..10).map(|_| ("k", "act")).collect(); + let rows = pack_shortcuts(&keys, 12); + assert_eq!(rows.len(), SHORTCUT_ROWS_MAX); + assert_eq!(texts(&rows)[0], "[k → act]"); + } + #[test] fn progress_bar_splits_fill_and_track() { let bar = format_progress_bar(0.25, 20, 25); diff --git a/website/src/components/tui/data.ts b/website/src/components/tui/data.ts index 62e5f33..154510b 100644 --- a/website/src/components/tui/data.ts +++ b/website/src/components/tui/data.ts @@ -182,6 +182,11 @@ export const SHORTCUTS: readonly (readonly [string, string])[] = [ ['c', 'chat'], ['/', 'filter'], ['a/A', 'select all/none'], - ['1-3', 'focus pane'], + ['h/l', 'scroll name'], + ['g/G', 'top/end'], + ['Tab/1-3', 'pane'], + ['z', 'zoom'], + ['w', 'hide'], + ['?', 'keys'], ['q', 'quit'], ] diff --git a/website/src/components/tui/hero-terminal.tsx b/website/src/components/tui/hero-terminal.tsx index 5806214..02221bf 100644 --- a/website/src/components/tui/hero-terminal.tsx +++ b/website/src/components/tui/hero-terminal.tsx @@ -37,45 +37,44 @@ export const HeroTerminal = () => { > -
-
- +
+ + {demo.searching && ( +
+ searching the network + +
+ )}
-
-
- +
+ - {demo.searching && ( -
- searching the network - -
- )}
- -
-
- -
-
- -
+
+ +
+
+
diff --git a/website/src/components/tui/panes.tsx b/website/src/components/tui/panes.tsx index 4e5854b..7bb8344 100644 --- a/website/src/components/tui/panes.tsx +++ b/website/src/components/tui/panes.tsx @@ -6,7 +6,7 @@ import { TuiPane } from './chrome' import type { ResultRow, SearchRow, TransferRow, TransferStatus } from './data' const RESULT_COLS = '30px 1fr 74px 92px 78px 74px 42px' -const TRANSFER_COLS = '48px 1fr 96px 150px 74px' +const TRANSFER_COLS = '48px 1fr 96px 168px 74px' interface SearchesPaneProps { searches: readonly SearchRow[] diff --git a/website/src/pages/docs.tsx b/website/src/pages/docs.tsx index 0c23098..0142507 100644 --- a/website/src/pages/docs.tsx +++ b/website/src/pages/docs.tsx @@ -25,10 +25,20 @@ const GLOBAL_KEYS: readonly (readonly [string, string])[] = [ ['i', 'inbox (unread counter)'], ['/', 'filter the current list'], ['a / A', 'select all / none'], - ['1–3', 'focus a pane'], + ['?', 'every key, grouped by pane'], ['q', 'quit'], ] +const PANE_KEYS: readonly (readonly [string, string])[] = [ + ['Tab · ⇧Tab', 'next / previous pane'], + ['1–3', 'focus a pane, and bring it back if hidden'], + ['z', 'zoom the focused pane to the whole window'], + ['w', 'hide the focused pane; the others widen'], + ['↑ ↓ · j k', 'move a row in any list'], + ['Home End · g G', 'first / last row'], + ['PgUp PgDn · ^u ^d', 'a page, or half a page'], +] + const ROOM_KEYS: readonly (readonly [string, string])[] = [ ['Enter', 'join room / send message'], ['Tab · ⇧Tab', 'switch between open rooms'], @@ -40,16 +50,16 @@ const ROOM_KEYS: readonly (readonly [string, string])[] = [ ] const PANES: readonly { num?: number; title: string; body: string }[] = [ + { + num: 2, + title: 'Results', + body: 'Files from the network across the whole width: size, user, bitrate, speed, free slots. Space selects, Enter queues a download. h/l or ←/→ scroll a long name, 0 and $ jump to either end.', + }, { num: 1, title: 'Searches', body: 'Your queries and how many results each returned. Press s to search the network; each search keeps its own result set.', }, - { - num: 2, - title: 'Results', - body: 'Files from the network: size, user, bitrate, speed, free slots. Space selects, Enter queues a download. h/l or ←/→ scroll a long name, 0 and $ jump to either end; Home/End, g/G, PgUp/PgDn and Ctrl-f/b/d/u move through the list.', - }, { num: 3, title: 'Downloads / Uploads', @@ -106,8 +116,10 @@ const FirstRun = () => ( const Layout = () => (
- The whole client is these four boxes. Press 1–3 to focus - one; the focused pane’s legend turns green. + The whole client is these four boxes: results across the top, the rest in a row + underneath. Tab or 1–3 focuses one and its legend + turns green; z gives it the whole window, w hides it so the + others widen.
@@ -127,7 +139,8 @@ const Layout = () => ( const Keys = () => (
- The bottom bar always shows the keys for where you are. The essentials: + The bottom bar always shows the keys for where you are, and ? lists every + one. The essentials:
@@ -138,6 +151,16 @@ const Keys = () => ( ))}
+
+ panes and lists +
+ {PANE_KEYS.map(([combo, description]) => ( + + ))} +
+
+
+
in the chat-rooms popup (press c) From 478d976776ff6de83f36a1491b55a8273dd64641 Mon Sep 17 00:00:00 2001 From: Michel de Graaf Date: Mon, 7 Sep 2026 08:16:20 +0000 Subject: [PATCH 2/3] feat(tui): scroll long names in every list and page through popups and chat The sideways keys the results had (h/l, arrows, 0 and $) now scroll a transfer's file name and a query too, each list on its own. The browse tree and the room list take Home/End, PgUp/PgDn and the vim jumps like the main lists, and a chat log, room or private, pages back through its history and holds its place while new messages arrive; what arrives meanwhile counts as unread until the reader returns to the end. --- README.md | 4 +- fixtures/demo.svg | 2 +- soulseek-rs/src/models/app_state.rs | 146 +++++++++++++++ soulseek-rs/src/models/mod.rs | 2 +- soulseek-rs/src/models/rooms.rs | 8 + soulseek-rs/src/ui/main_tui/browse.rs | 16 +- soulseek-rs/src/ui/main_tui/input.rs | 198 +++++++++++++++----- soulseek-rs/src/ui/main_tui/mod.rs | 208 ++++++++++++++++++++- soulseek-rs/src/ui/main_tui/render.rs | 30 ++- soulseek-rs/src/ui/main_tui/rooms.rs | 43 ++++- soulseek-rs/src/ui/panes/chat_pane.rs | 28 ++- soulseek-rs/src/ui/panes/downloads_pane.rs | 68 +++++-- soulseek-rs/src/ui/panes/mod.rs | 8 +- soulseek-rs/src/ui/panes/name_scroll.rs | 126 +++++++++++++ soulseek-rs/src/ui/panes/results_pane.rs | 61 +----- soulseek-rs/src/ui/panes/rooms_pane.rs | 26 +-- soulseek-rs/src/ui/panes/searches_pane.rs | 39 ++-- website/src/pages/docs.tsx | 3 +- 18 files changed, 841 insertions(+), 175 deletions(-) create mode 100644 soulseek-rs/src/ui/panes/name_scroll.rs diff --git a/README.md b/README.md index df879aa..ee80220 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,9 @@ Searches, Downloads and Info share the row underneath. `Tab`/`Shift-Tab` or `1`–`3` move the focus between panes, `z` zooms the focused pane to the whole window, and `w` hides it so the others widen (its number brings it back). Every list takes `↑`/`↓`, `Home`/`End`, `PgUp`/`PgDn` and the vim keys, and -`?` shows every key for where you are. +`h`/`l` or `←`/`→` scroll a long file name or query sideways, `0` and `$` to +either end. `PgUp`/`PgDn` scroll a chat log back through its history, and `?` +shows every key for where you are. ### Daemon mode diff --git a/fixtures/demo.svg b/fixtures/demo.svg index 0f07359..dde3801 100644 --- a/fixtures/demo.svg +++ b/fixtures/demo.svg @@ -1 +1 @@ -╭Status────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│soulseek-rs🦀v14.1.2Downloads:0active,0completed,0failed,0que│╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╭[2]Results───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│soulseek-rsv14.1.2││││Noresultsyet.││Pickasearchin[1]Searches,orstartanewone.│╭[1]Searches────────────────────╮╭[3]Downloads/Uploads──────────────────────────────────────────────╮╭[Info]─────────────────────────────────╮│StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││Selectaresultortransferfordetails│││││││╰──────────────────────────────────╯╰─────────────────────────────────────────────────────────────────────╯╰─────────────────────────────────────────╯╭Shortcuts·Sharing:~/Music──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│[s→search][Enter→results][d→remove][m→message][i→inbox][c→chat][b→browseuser][Tab/1-3→pane][z→zoom][w→hide]││[?→keys][q→quit]│╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│search:│╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│[Type→searchterm][←→→movecursor][Backspace/Del→edit][Enter→search][Esc→cancel]││search:public││search:publicdomain││search:publicdomainfield││search:publicdomainfieldrecordings│╭[2]Results:publicdomainfieldrecordings───────────────────────────────────────────────────────────────────────────────────────────────────────╮│soulseek-rsv14.1.2││││Noresultsyet.││Pickasearchin[1]Searches,orstartanewone.│╭[1]Searches────────────────────╮╭[3]Downloads/Uploads──────────────────────────────────────────────╮╭[Info]─────────────────────────────────╮│StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││Selectaresultortransferfordetails││›Activepublic domain fi…│││││││││││╰──────────────────────────────────╯╰─────────────────────────────────────────────────────────────────────╯╰─────────────────────────────────────────╯│[Space→select][Enter→download][b→browseowner][c→chat][/→filter][a/A→selectall/none][h/l→scrollname][g/G→top/end]││[Tab/1-3→pane][z→zoom][w→hide][?→keys][q→quit]││✓FilenameSizeUserBitrateSpeedSlots││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\01 - Coastal D30.5 MBloopdigger675 kbps3.35 MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\02-Reservoir22.6MBloopdigger569kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-Marshat48.0MBloopdigger920kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\04-TramDepo32.2MBloopdigger725kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourB52.5MBloopdigger902kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\06-RainonZ30.4MBloopdigger804kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\cover.jpg0.4MBloopdigger-3.35MB/s1││StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││01-CoastalDawn(SunriseTake,Long││›Activepublic domain fi7││││Version).flac││││││@@gufoo/FieldRecordings/CoastalDawn││││││Sessions(2019)[FLAC24-96]││││││Userloopdigger││││││Size30.5MB││││││Bitrate675kbps││││││Length6m52s││││││Freeslots1available││││││Uploadspeed3.4MB/s││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0117.4MBtapehiss836kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0238.0MBtapehiss741kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0334.6MBtapehiss753kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0418.7MBtapehiss723kbps2.1MB/s2││›Activepublic domain fi11││││Version).flac││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\01-CoastalD30.5MBloopdigger675kbps3.35MB/s1││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\02 - Reservoir22.6 MBloopdigger569 kbps3.35 MB/s1││StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││02-ReservoirWind.flac││›Activepublic domain fi11││││@@gufoo/FieldRecordings/CoastalDawn││││││Size22.6MB││││││Bitrate569kbps││││││Length5m18s││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at48.0 MBloopdigger920 kbps3.35 MB/s1││StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││03-MarshatNight(Long).flac││││││Size48.0MB││││││Bitrate920kbps││││││Length8m21s││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-9.4MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-5.1MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-19.5MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-0.1MBnectoUA-1.05MB/s0││›Activepublic domain fi15││││@@gufoo/FieldRecordings/CoastalDawn││›[✓]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at48.0 MBloopdigger920 kbps3.35 MB/s1││[✓]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-Marshat48.0MBloopdigger920kbps3.35MB/s1││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\04 - Tram Depo32.2 MBloopdigger725 kbps3.35 MB/s1││StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││04-TramDepot,06h.flac││││││Size32.2MB││││││Bitrate725kbps││││││Length6m06s││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour B52.5 MBloopdigger902 kbps3.35 MB/s1││StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││05-HarbourBells.flac││││││Size52.5MB││││││Bitrate902kbps││││││Length9m33s││›[✓]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour B52.5 MBloopdigger902 kbps3.35 MB/s1││›Donepublic domain fi15││││@@gufoo/FieldRecordings/CoastalDawn││soulseek-rs🦀v14.1.2Downloads:0active,0completed,0failed,2que││StatusQueryResults││StatusFilenameUserProgressSpeed││05-HarbourBells.flac││›Donepublic domain fi15││›⋯@@gufoo\FloopdiggerQueued-││@@gufoo/FieldRecordings/CoastalDawn││││⋯@@gufoo\FloopdiggerQueued-││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que2.8/100.5MB·18.61MB/s·[░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]2%│╭[2]Results:publicdomainfieldrecordings───────────────────────────────────────────────────────────────────────────────────────────────────────╮│✓FilenameSizeUserBitrateSpeedSlots││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\01-CoastalD30.5MBloopdigger675kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\02-Reservoir22.6MBloopdigger569kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-Marshat48.0MBloopdigger920kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\04-TramDepo32.2MBloopdigger725kbps3.35MB/s1││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour B52.5 MBloopdigger902 kbps3.35 MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\06-RainonZ30.4MBloopdigger804kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\cover.jpg0.4MBloopdigger-3.35MB/s1││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0117.4MBtapehiss836kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0238.0MBtapehiss741kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0334.6MBtapehiss753kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0418.7MBtapehiss723kbps2.1MB/s2││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-9.4MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-5.1MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-19.5MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-0.1MBnectoUA-1.05MB/s0│╭[1]Searches────────────────────╮╭[3]Downloads/Uploads──────────────────────────────────────────────╮╭[Info]─────────────────────────────────╮│StatusQueryResults││StatusFilenameUserProgressSpeed││03-MarshatNight(Long).flac││›Donepublic domain fi15││›↯@@gufoo\Floopdigger1.3 MB/48.0 MB (2%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger1.5MB/52.5MB(2%)9.7MB/s││Sessions(2019)[FLAC24-96]││││││││││││Userloopdigger││││││Size48.0MB││││││StatusInprogress││││││Saveto││││││/home/demo/Music/Soulseek││││││Bitrate920kbps││││││Length8m21s│╰──────────────────────────────────╯╰─────────────────────────────────────────────────────────────────────╯╰─────────────────────────────────────────╯│[p→pause/resume][x→cancel][r→retryfailed][d→deletequeued/done][c→clearfinished][b→browseuser][Tab/1-3→pane][z→zoom]││[w→hide][?→keys][q→quit]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que5.6/100.5MB·18.61MB/s·[█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]5%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger2.7 MB/48.0 MB (5%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger2.9MB/52.5MB(5%)9.7MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que11.2/100.5MB·18.61MB/s·[███░░░░░░░░░░░░░░░░░░░░░░░░░░░░]11%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger5.3 MB/48.0 MB (11%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger5.8MB/52.5MB(11%)9.7MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que19.5/100.5MB·18.61MB/s·[██████░░░░░░░░░░░░░░░░░░░░░░░░░]19%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger9.3 MB/48.0 MB (19%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger10.2MB/52.5MB(19%)9.7MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que25.1/100.5MB·18.61MB/s·[███████░░░░░░░░░░░░░░░░░░░░░░░░]25%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger12.0 MB/48.0 MB (25%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger13.1MB/52.5MB(25%)9.7MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que30.7/100.5MB·18.61MB/s·[█████████░░░░░░░░░░░░░░░░░░░░░░]30%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger14.7 MB/48.0 MB (30%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger16.0MB/52.5MB(30%)9.7MB/s││Sessions(2019)[FLAC24-96]││›Donepublic domain fi15││›↯@@gufoo\Floopdigger17.3 MB/48.0 MB (36%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger20.4MB/52.5MB(38%)9.7MB/s││Sessions(2019)[FLAC24-96]││›Donepublic domain fi15││›↯@@gufoo\Floopdigger18.7 MB/48.0 MB (38%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↑↯HarbourBcrateminer5.3MB/41.0MB(12%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que41.9/100.5MB·18.61MB/s·[████████████░░░░░░░░░░░░░░░░░░░]41%│╭[3]Downloads/Uploads─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│StatusFilenameUserProgressSpeed││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger20.0 MB/48.0 MB (41%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger21.9MB/52.5MB(41%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer5.7MB/41.0MB(13%)3.7MB/s││[p→pause/resume][x→cancel][r→retryfailed][d→deletequeued/done][c→clearfinished][b→browseuser][Tab/1-3→pane][z→unzoom]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que50.2/100.5MB·18.61MB/s·[███████████████░░░░░░░░░░░░░░░░]50%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger24.0 MB/48.0 MB (50%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger26.2MB/52.5MB(50%)9.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que55.8/100.5MB·18.61MB/s·[█████████████████░░░░░░░░░░░░░░]55%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger26.7 MB/48.0 MB (55%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger29.2MB/52.5MB(55%)9.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que61.4/100.5MB·18.61MB/s·[██████████████████░░░░░░░░░░░░░]61%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger29.3 MB/48.0 MB (61%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger32.1MB/52.5MB(61%)9.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que67.0/100.5MB·18.61MB/s·[████████████████████░░░░░░░░░░░]66%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger32.0 MB/48.0 MB (66%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger35.0MB/52.5MB(66%)9.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que72.6/100.5MB·18.61MB/s·[██████████████████████░░░░░░░░░]72%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger34.7 MB/48.0 MB (72%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger37.9MB/52.5MB(72%)9.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que81.0/100.5MB·18.61MB/s·[████████████████████████░░░░░░░]80%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger38.7 MB/48.0 MB (80%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger42.3MB/52.5MB(80%)9.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que83.8/100.5MB·18.61MB/s·[█████████████████████████░░░░░░]83%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger40.0 MB/48.0 MB (83%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger43.8MB/52.5MB(83%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer14.0MB/41.0MB(34%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que89.3/100.5MB·18.61MB/s·[███████████████████████████░░░░]88%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger42.7 MB/48.0 MB (88%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger46.7MB/52.5MB(88%)9.7MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que94.9/100.5MB·18.61MB/s·[█████████████████████████████░░]94%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger45.3 MB/48.0 MB (94%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger49.6MB/52.5MB(94%)9.7MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que97.7/100.5MB·18.61MB/s·[██████████████████████████████░]97%││StatusQueryResults││StatusFilenameUserProgressSpeed││03-MarshatNight(Long).flac││›Donepublic domain fi15││›↯@@gufoo\Floopdigger46.7 MB/48.0 MB (97%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger51.0MB/52.5MB(97%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer17.0MB/41.0MB(41%)3.7MB/s││││││││Userloopdigger││││││Size48.0MB││││││StatusInprogress││││││Saveto││││││/home/demo/Music/Soulseek││││││Bitrate920kbps││││││Length8m21s││soulseek-rs🦀v14.1.2Downloads:0active,2completed,0failed,0que││›Donepublic domain fi15││›✓@@gufoo\FloopdiggerCompleted-││@@gufoo/FieldRecordings/CoastalDawn││││✓@@gufoo\FloopdiggerCompleted-││Sessions(2019)[FLAC24-96]││││││StatusCompleted││││↑↯HarbourBcrateminer19.9MB/41.0MB(48%)3.7MB/s│││╭[3]Downloads/Uploads─────────────────────────────────────────────────────────────────────╮╭[Info]──────────────────────────────────────────────╮│StatusFilenameUserProgressSpeed││05-HarbourBells.flac││›✓@@gufoo\Field Recordings\CoastalloopdiggerCompleted-││@@gufoo/FieldRecordings/CoastalDawnSessions││✓@@gufoo\FieldRecordings\CoastalloopdiggerCompleted-││(2019)[FLAC24-96]││↑↯HarbourBells(LiveattheQuay)crateminer20.3MB/41.0MB(49%)3.7MB/s││││││Userloopdigger││││Size52.5MB││││Bitrate902kbps││││Length9m33s││││││││Freeslots1available││││Uploadspeed3.4MB/s│╰────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────╯│↑↯HarbourBells(LiveattheQuay)crateminer27.6MB/41.0MB(67%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer29.2MB/41.0MB(71%)3.7MB/s││││[]…eldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\01-CoastalDawn(Sunr30.5MBloopdigger675kbps3.35MB/s1││[]…eldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\02-ReservoirWind.fla22.6MBloopdigger569kbps3.35MB/s1││[]…eldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(Lo48.0MBloopdigger920kbps3.35MB/s1││[]…eldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\04-TramDepot,06h.fl32.2MBloopdigger725kbps3.35MB/s1││›[ ]…eld Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flac52.5 MBloopdigger902 kbps3.35 MB/s1││[]…eldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\06-RainonZincRoof.30.4MBloopdigger804kbps3.35MB/s1││[]…less\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\01-EmptyC17.4MBtapehiss836kbps2.1MB/s2││[]…less\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\02-Forest38.0MBtapehiss741kbps2.1MB/s2││[]…less\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\03-Quarry34.6MBtapehiss753kbps2.1MB/s2││[]…less\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\04-NightF18.7MBtapehiss723kbps2.1MB/s2││[]…mplete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-BY)\01-9.4MBnectoUA320kbps1.05MB/s0││[]…mplete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-BY)\02-5.1MBnectoUA320kbps1.05MB/s0││[]…mplete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-BY)\03-19.5MBnectoUA320kbps1.05MB/s0││[]…mplete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-BY)\readm0.1MBnectoUA-1.05MB/s0││↑↯HarbourBells(LiveattheQuay)crateminer35.3MB/41.0MB(86%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer38.2MB/41.0MB(93%)3.7MB/s││││││↑↯HarbourBcrateminer38.5MB/41.0MB(94%)3.7MB/s││││││↑✓HarbourBcrateminerCompleted-││││›[ ]@@gufoo\Field Recor╭Keys─────────────────────────────────────────────────────────────────────────────────────────╮bps3.35 MB/s1││[]@@gufoo\FieldRecor│PanesSearches│bps3.35MB/s1││[]@@gufoo\FieldRecor│Tab/Shift-Tabnext/previouspaneEntershowitsresults│3.35MB/s1││[]Music\lossless\Publ│123focusapane,hiddenornotdremovethesearch│bps2.1MB/s2││[]Music\lossless\Publ│zzoomthefocusedpaneCcleareverysearch│bps2.1MB/s2││[]Music\lossless\Publ│whidethefocusedpanecchatrooms│bps2.1MB/s2││[]Music\lossless\Publ│Escleavezoom│bps2.1MB/s2││[]@@hnttf\complete\Ex│clickfocusapaneResults│bps1.05MB/s0││[]@@hnttf\complete\Ex│Spaceselect/deselect│bps1.05MB/s0││[]@@hnttf\complete\Ex│Anywherea/Aselectall/none│bps1.05MB/s0││[]@@hnttf\complete\Ex│ssearchthenetworkEnterdownloadtheselection│1.05MB/s0│││bbrowseauser'sfiles/filter,Enterkeepsit,Escclears││││mcomposeaprivatemessagehl/←→scrollthefilename││││iinbox0/$start/endofthefilename││││osettingsbbrowsetheowner││╰─────────────────────────│?thislistcchatrooms│──────────────────────────╯╭[1]Searches───────────│qquit│──────────────────────────╮│StatusQuery│Downloads/Uploads│Night(Long).flac││›Donepublic domain fi│Anylistppause/resume│Recordings/CoastalDawn│││↑↓/kjmoveonerowxcancel│)[FLAC24-96]│││HomeEnd/gGfirst/lastrowrretryafailedone││││PgUpPgDn/^b^fonepageddeleteaqueuedorfinishedone│loopdigger│││^u^dhalfapageccleareveryfinishedone│48.0MB││╰───────────────────────────────────────────────────────────────────────────────────────────────╯Completed││[?/Esc→close]│╰────────────────────────────────────────────────────────────────╭[1]Searches────────────────────╮╭[3]Downloads/Uploads───│││││╭Shortcuts·Sharing:~/Music───────────────────────────────────────────────────────────────────────────│[s→search][Enter→results][d→remove][m→message][i→inbox][c→chat][b→browseuser][Tab/1-3→pane│StatusQueryResults╮╭[3]Downloads/Uploads──────────────────────────────────────────────╮╭[Info]─────────────────────────────────╮╭│││││╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│[Type→searchterm][←→→movecursor][Backspace/Del→edit][Enter→search]││search:p││search:pu││search:pub││search:publ││search:publi││search:publicd││search:publicdo││search:publicdom││search:publicdoma││search:publicdomai││search:publicdomainf││search:publicdomainfi││search:publicdomainfie││search:publicdomainfiel││search:publicdomainfieldr││search:publicdomainfieldre││search:publicdomainfieldrec││search:publicdomainfieldreco││search:publicdomainfieldrecor││search:publicdomainfieldrecord││search:publicdomainfieldrecordi││search:publicdomainfieldrecordin││search:publicdomainfieldrecording│╰────────────s────────────────────╮╭[3]Downloads/Uploads──────────────────────────────────────────────╮╭[Info]─────────────────────────────────╮│StatusQueryResults│││││││││││╰──────────────────────────────────╯╰──────────────────────││[Space→select][Enter→download]───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│[Tab/1-3→pane][z][←→→movecursor][Backspace/Del→edit][Enter→search][Esc→cancel]││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2││││││Length││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\041││›Activepublic domain fi11││││@@gufoo/FieldRecordings/CoastalDa││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-0││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0418.7MBtapehiss723kbps2.1MB/s2││›Donepublic domain fi15││›↯@@gufoo\FloopdiggerQueued-││@@gufoo/FieldRecordings/CoastalDawn││││││Length3.4MB/s││[p→pause/resume][x→cancel][r→retryfailed][d→deletequeued/done][c→clearfinished][one][h/l→scrollname][g/G→top/end]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que8.4/100.5MB·18.61MB/s·[██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]8%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger4.0 MB/48.0 MB (8%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger4.4MB/52.5MB(8%)9.7MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que14.0/100.5MB·18.61MB/s·[████░░░░░░░░░░░░░░░░░░░░░░░░░░░]13%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger6.7 MB/48.0 MB (13%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger7.3MB/52.5MB(13%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer0.2MB/41.0MB(0%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que16.8/100.5MB·18.61MB/s·[█████░░░░░░░░░░░░░░░░░░░░░░░░░░]16%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger8.0 MB/48.0 MB (16%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger8.8MB/52.5MB(16%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer0.6MB/41.0MB(1%)3.7MB/s││││││↑↯HarbourBcrateminer1.0MB/41.0MB(2%)3.7MB/s││││││↑↯HarbourBcrateminer1.4MB/41.0MB(3%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que22.3/100.5MB·18.61MB/s·[██████░░░░░░░░░░░░░░░░░░░░░░░░░]22%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger10.7 MB/48.0 MB (22%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger11.7MB/52.5MB(22%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer1.7MB/41.0MB(4%)3.7MB/s││││││↑↯HarbourBcrateminer2.1MB/41.0MB(5%)3.7MB/s││││││↑↯HarbourBcrateminer2.5MB/41.0MB(6%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que27.9/100.5MB·18.61MB/s·[████████░░░░░░░░░░░░░░░░░░░░░░░]27%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger13.3 MB/48.0 MB (27%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger14.6MB/52.5MB(27%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer2.9MB/41.0MB(7%)3.7MB/s││││││↑↯HarbourBcrateminer3.3MB/41.0MB(8%)3.7MB/s││││││↑↯HarbourBcrateminer3.7MB/41.0MB(9%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que33.5/100.5MB·18.61MB/s·[██████████░░░░░░░░░░░░░░░░░░░░░]33%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger16.0 MB/48.0 MB (33%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger17.5MB/52.5MB(33%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer4.1MB/41.0MB(9%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que36.3/100.5MB·18.61MB/s·[███████████░░░░░░░░░░░░░░░░░░░░]36%││││↯@@gufoo\Floopdigger19.0MB/52.5MB(36%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer4.5MB/41.0MB(10%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que37.7/100.5MB·18.61MB/s·[███████████░░░░░░░░░░░░░░░░░░░░]37%││││↑↯HarbourBcrateminer4.9MB/41.0MB(11%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que39.1/100.5MB·18.61MB/s·[████████████░░░░░░░░░░░░░░░░░░░]38%││›↯@@gufoo\ldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\01-CoastalD30.5MBloopdigger675kbps3.35MB/s1││↑↯HarbourBells(LiveattheQuay).flaccrateminer5.7MB/41.0MB(13%)3.7MB/s││FieldRecordings,Vol.2(CreativeCommonsZero)\0117.4MBtapehiss836kbps2.1MB/s2││dings2004(netlabel,CC-9.4MBnectoUA320kbps1.05MB/s0││─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│││↑↯HarbourBcrateminer5.3MB/41.0MB(12%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay).flaccrateminer6.0MB/41.0MB(14%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que44.7/100.5MB·18.61MB/s·[█████████████░░░░░░░░░░░░░░░░░░]44%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger21.3 MB/48.0 MB (44%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger23.3MB/52.5MB(44%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer6.4MB/41.0MB(15%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que47.5/100.5MB·18.61MB/s·[██████████████░░░░░░░░░░░░░░░░░]47%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger22.7 MB/48.0 MB (47%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger24.8MB/52.5MB(47%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer6.8MB/41.0MB(16%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer7.1MB/41.0MB(17%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer7.5MB/41.0MB(18%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que53.0/100.5MB·18.61MB/s·[████████████████░░░░░░░░░░░░░░░]52%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger25.3 MB/48.0 MB (52%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger27.7MB/52.5MB(52%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer7.9MB/41.0MB(19%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer8.3MB/41.0MB(20%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer8.7MB/41.0MB(21%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que58.6/100.5MB·18.61MB/s·[██████████████████░░░░░░░░░░░░░]58%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger28.0 MB/48.0 MB (58%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger30.6MB/52.5MB(58%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer9.1MB/41.0MB(22%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer9.4MB/41.0MB(23%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer9.8MB/41.0MB(23%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que64.2/100.5MB·18.61MB/s·[███████████████████░░░░░░░░░░░░]63%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger30.7 MB/48.0 MB (63%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger33.5MB/52.5MB(63%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer10.2MB/41.0MB(24%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer10.6MB/41.0MB(25%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer11.0MB/41.0MB(26%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que69.8/100.5MB·18.61MB/s·[█████████████████████░░░░░░░░░░]69%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger33.3 MB/48.0 MB (69%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger36.5MB/52.5MB(69%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer11.4MB/41.0MB(27%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer11.7MB/41.0MB(28%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer12.1MB/41.0MB(29%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que75.4/100.5MB·18.61MB/s·[███████████████████████░░░░░░░░]75%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger36.0 MB/48.0 MB (75%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger39.4MB/52.5MB(75%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer12.5MB/41.0MB(30%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que78.2/100.5MB·18.61MB/s·[████████████████████████░░░░░░░]77%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at Night (loopdigger37.3 MB/48.0 MB (77%)8.9 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourBells.flloopdigger40.8MB/52.5MB(77%)9.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer12.9MB/41.0MB(31%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer13.3MB/41.0MB(32%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer13.7MB/41.0MB(33%)3.7MB/s││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\06-RainonZ││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0238.0MBtapehiss741kbps││[]@@hnttf\compl│╰──────────────────────────────││StatusQueryResults││││││││↑↯HarbourBcrateminer14.2MB/41.0MB(34%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que86.5/100.5MB·18.61MB/s·[██████████████████████████░░░░░]86%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger41.3 MB/48.0 MB (86%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger45.2MB/52.5MB(86%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer14.6MB/41.0MB(35%)3.7MB/s││││││↑↯HarbourBcrateminer15.0MB/41.0MB(36%)3.7MB/s││││││↑↯HarbourBcrateminer15.4MB/41.0MB(37%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que92.1/100.5MB·18.61MB/s·[████████████████████████████░░░]91%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger44.0 MB/48.0 MB (91%)8.9 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger48.1MB/52.5MB(91%)9.7MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer15.8MB/41.0MB(38%)3.7MB/s││││││↑↯HarbourBcrateminer16.2MB/41.0MB(39%)3.7MB/s││││││↑↯HarbourBcrateminer16.6MB/41.0MB(40%)3.7MB/s││││││↑↯HarbourBcrateminer16.6MB/41.0MB(40%)3.7MB/s│││╰──────────────────────────────────╯╰─────────────────────────────────────────────────────────────────────╯╰─────────────────────────────────────────╯│[s→search][Enter→results][d→remove][m→message][i→inbox]done][c→clearfinished][b→browseuser][Tab/1-3→pane][z→zoom]││││↑↯HarbourBcrateminer17.2MB/41.0MB(41%)3.7MB/s││││││↑↯HarbourBcrateminer17.6MB/41.0MB(42%)3.7MB/s││││││↑↯HarbourBcrateminer18.0MB/41.0MB(43%)3.7MB/s││││││↑↯HarbourBcrateminer18.4MB/41.0MB(44%)3.7MB/s││││││↑↯HarbourBcrateminer18.8MB/41.0MB(45%)3.7MB/s││││││↑↯HarbourBcrateminer19.2MB/41.0MB(46%)3.7MB/s││││││↑↯HarbourBcrateminer19.5MB/41.0MB(47%)3.7MB/s│││╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│StatusFilenameUserProgressSpeed││05d││03-MarshatNight(Long).flac││✓@@gufoo\FieldRecordings\CoastalloopdiggerCompleted-││(2019)[FLAC24-9ions(2019)[FLAC24-96]││││StatusCompleted││││Uploadspeed8m21s││[Space→select][Enter→download][b→browseowner][c[i→inbox][c→chat][b→browseuser][Tab/1-3→pane][z→zoom][w→hide]││[Tab/1-3→pane][z→zoom][w→hide][?→││↑↯HarbourBells(LiveattheQuay)crateminer20.6MB/41.0MB(50%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer21.0MB/41.0MB(51%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer21.4MB/41.0MB(52%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer21.8MB/41.0MB(53%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer22.2MB/41.0MB(54%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer22.5MB/41.0MB(54%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer22.9MB/41.0MB(55%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer23.3MB/41.0MB(56%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer23.7MB/41.0MB(57%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer24.1MB/41.0MB(58%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer24.5MB/41.0MB(59%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer24.9MB/41.0MB(60%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer25.3MB/41.0MB(61%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer25.7MB/41.0MB(62%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer26.1MB/41.0MB(63%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer26.5MB/41.0MB(64%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer26.8MB/41.0MB(65%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer27.2MB/41.0MB(66%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer28.0MB/41.0MB(68%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer28.4MB/41.0MB(69%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer28.8MB/41.0MB(70%)3.7MB/s││││[]…less\PublicDomainFieldRecordings,Vol.2(CreativeCreativeCommonsZero)\0334.6MBtapehiss753kbps2.1MB/s2││↑↯HarbourBells(LiveattheQuay)crateminer29.6MB/41.0MB(72%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer29.8MB/41.0MB(72%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer30.2MB/41.0MB(73%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer30.6MB/41.0MB(74%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer31.0MB/41.0MB(75%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer31.4MB/41.0MB(76%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer31.8MB/41.0MB(77%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer32.2MB/41.0MB(78%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer32.6MB/41.0MB(79%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer33.0MB/41.0MB(80%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer33.4MB/41.0MB(81%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer33.8MB/41.0MB(82%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer34.1MB/41.0MB(83%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer34.5MB/41.0MB(84%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer34.9MB/41.0MB(85%)3.7MB/s││││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommons-Quarry34.6MBtapehiss753kbps2.1MB/s2││↑↯HarbourBells(LiveattheQuay)crateminer35.7MB/41.0MB(87%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer35.8MB/41.0MB(87%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer36.2MB/41.0MB(88%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer36.6MB/41.0MB(89%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer37.0MB/41.0MB(90%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer37.4MB/41.0MB(91%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer37.8MB/41.0MB(92%)3.7MB/s│││╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│││││Userger││[sace→select][Enter→download][b→browseowner][c→chat][/→filter][a/A→selectall/none][h/l→scrollname][g/G→top/end]││[s→search][Enter→results][d→remove][m→message][i→inbox][c→chat][b→browseuser][Tab/1-3→pane][zame][g/G→top/end]││││↑↯HarbourBcrateminer38.8MB/41.0MB(94%)3.7MB/s││││││↑↯HarbourBcrateminer39.2MB/41.0MB(95%)3.7MB/s││││││↑↯HarbourBcrateminer39.6MB/41.0MB(96%)3.7MB/s││││││↑↯HarbourBcrateminer40.0MB/41.0MB(97%)3.7MB/s││││││↑↯HarbourBcrateminer40.4MB/41.0MB(98%)3.7MB/s││││││↑↯HarbourBcrateminer40.7MB/41.0MB(99%)3.7MB/s││││[]@@hnttf\complete\Ex│clickfocusapaneResults320kbps1.05MB/s0│││m│╰─────────────────────────│?thislist──────────────────────────────╮╭[Info]─────────────────────────────────╮│›││✓@@gufoo\FloopdiggerCompleted-││Sessions(2019)[FLAC24-96]│││HomeEnd/gGfirst/lastrowrretryafailedone│loopdigger││││││Savetoo/Music/Soulseek│╭Shortcuts·Sharing:~/Music────────────────────────────────────────────────────────────────────────────────────e][z→zoom][w→hide]││[]Music\lossless\PublicDomainzoomthefocusedpaneCcleareverysearch│bps2.1MB/s2││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-5.1MBnectoUA│bps1.05MB/s0│╰────────────────────────────────────────────────────────bbrowsetheowner│││listppause/resume│Recordings/CoastalDawn│││n/^b^fonepageddeleteaqueuedorfinishedone│loopdigger│╰────────││││││[s→search][Enter→results][d→remove][m────────────────────────────────────────────────────────────────────────────────────────────────────╮│[?→keys][qe]│ \ No newline at end of file +╭Status────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│soulseek-rs🦀v14.1.2Downloads:0active,0completed,0failed,0que│╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╭[2]Results───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│soulseek-rsv14.1.2││││Noresultsyet.││Pickasearchin[1]Searches,orstartanewone.│╭[1]Searches────────────────────╮╭[3]Downloads/Uploads──────────────────────────────────────────────╮╭[Info]─────────────────────────────────╮│StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││Selectaresultortransferfordetails│││││││╰──────────────────────────────────╯╰─────────────────────────────────────────────────────────────────────╯╰─────────────────────────────────────────╯╭Shortcuts·Sharing:~/Music──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│[s→search][Enter→results][d→remove][m→message][i→inbox][c→chat][b→browseuser][h/l→scrollquery][Tab/1-3→pane]││[z→zoom][w→hide][?→keys][q→quit]│╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│search:│╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│[Type→searchterm][←→→movecursor][Backspace/Del→edit][Enter→search][Esc→cancel]││search:public││search:publicdomain││search:publicdomainfield││search:publicdomainfieldrecordings│╭[2]Results:publicdomainfieldrecordings───────────────────────────────────────────────────────────────────────────────────────────────────────╮│soulseek-rsv14.1.2││││Noresultsyet.││Pickasearchin[1]Searches,orstartanewone.│╭[1]Searches────────────────────╮╭[3]Downloads/Uploads──────────────────────────────────────────────╮╭[Info]─────────────────────────────────╮│StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││Selectaresultortransferfordetails││›Activepublic domain fi…│││││││││││╰──────────────────────────────────╯╰─────────────────────────────────────────────────────────────────────╯╰─────────────────────────────────────────╯│[Space→select][Enter→download][b→browseowner][c→chat][/→filter][a/A→selectall/none][h/l→scrollname][g/G→top/end]││[Tab/1-3→pane][z→zoom][w→hide][?→keys][q→quit]││✓FilenameSizeUserBitrateSpeedSlots││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\01 - Coastal D30.5 MBloopdigger675 kbps3.35 MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\02-Reservoir22.6MBloopdigger569kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-Marshat48.0MBloopdigger920kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\04-TramDepo32.2MBloopdigger725kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\05-HarbourB52.5MBloopdigger902kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\06-RainonZ30.4MBloopdigger804kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\cover.jpg0.4MBloopdigger-3.35MB/s1││StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││01-CoastalDawn(SunriseTake,Long││›Activepublic domain fi7││││Version).flac││││││@@gufoo/FieldRecordings/CoastalDawn││││││Sessions(2019)[FLAC24-96]││││││Userloopdigger││││││Size30.5MB││││││Bitrate675kbps││││││Length6m52s││││││Freeslots1available││││││Uploadspeed3.4MB/s││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0117.4MBtapehiss836kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0238.0MBtapehiss741kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0334.6MBtapehiss753kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0418.7MBtapehiss723kbps2.1MB/s2││›Activepublic domain fi11││││Version).flac││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\01-CoastalD30.5MBloopdigger675kbps3.35MB/s1││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\02 - Reservoir22.6 MBloopdigger569 kbps3.35 MB/s1││StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││02-ReservoirWind.flac││›Activepublic domain fi11││││@@gufoo/FieldRecordings/CoastalDawn││││││Size22.6MB││││││Bitrate569kbps││││││Length5m18s││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at48.0 MBloopdigger920 kbps3.35 MB/s1││StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││03-MarshatNight(Long).flac││││││Size48.0MB││││││Bitrate920kbps││││││Length8m21s││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-9.4MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-5.1MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-19.5MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-0.1MBnectoUA-1.05MB/s0││›Activepublic domain fi15││││@@gufoo/FieldRecordings/CoastalDawn││›[✓]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\03 - Marsh at48.0 MBloopdigger920 kbps3.35 MB/s1││[✓]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-Marshat48.0MBloopdigger920kbps3.35MB/s1││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\04 - Tram Depo32.2 MBloopdigger725 kbps3.35 MB/s1││StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││04-TramDepot,06h.flac││││││Size32.2MB││││││Bitrate725kbps││││││Length6m06s││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour B52.5 MBloopdigger902 kbps3.35 MB/s1││StatusQueryResults││Notransfers.Selectfilesin[2]ResultsandpressEnter.││05-HarbourBells.flac││││││Size52.5MB││││││Bitrate902kbps││││││Length9m33s││›[✓]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour B52.5 MBloopdigger902 kbps3.35 MB/s1││›Donepublic domain fi15││││@@gufoo/FieldRecordings/CoastalDawn││soulseek-rs🦀v14.1.2Downloads:0active,0completed,0failed,2que││StatusQueryResults││StatusFilenameUserProgressSpeed││05-HarbourBells.flac││›Donepublic domain fi15││›⋯@@gufoo\FloopdiggerQueued-││@@gufoo/FieldRecordings/CoastalDawn││││⋯@@gufoo\FloopdiggerQueued-││Sessions(2019)[FLAC24-96]│╭[2]Results:publicdomainfieldrecordings───────────────────────────────────────────────────────────────────────────────────────────────────────╮│✓FilenameSizeUserBitrateSpeedSlots││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\01-CoastalD30.5MBloopdigger675kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\02-Reservoir22.6MBloopdigger569kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-Marshat48.0MBloopdigger920kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\04-TramDepo32.2MBloopdigger725kbps3.35MB/s1││›[ ]@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour B52.5 MBloopdigger902 kbps3.35 MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\06-RainonZ30.4MBloopdigger804kbps3.35MB/s1││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\cover.jpg0.4MBloopdigger-3.35MB/s1││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0117.4MBtapehiss836kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0238.0MBtapehiss741kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0334.6MBtapehiss753kbps2.1MB/s2││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0418.7MBtapehiss723kbps2.1MB/s2││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-9.4MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-5.1MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-19.5MBnectoUA320kbps1.05MB/s0││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-0.1MBnectoUA-1.05MB/s0│╭[1]Searches────────────────────╮╭[3]Downloads/Uploads──────────────────────────────────────────────╮╭[Info]─────────────────────────────────╮│StatusQueryResults││StatusFilenameUserProgressSpeed││05-HarbourBells.flac││›Donepublic domain fi15││›⋯@@gufoo\FloopdiggerQueued-││@@gufoo/FieldRecordings/CoastalDawn││││⋯@@gufoo\FloopdiggerQueued-││Sessions(2019)[FLAC24-96]││││││││││││Userloopdigger││││││Size52.5MB││││││StatusQueued││││││Saveto││││││/home/demo/Music/Soulseek││││││Bitrate902kbps││││││Length9m33s│╰──────────────────────────────────╯╰─────────────────────────────────────────────────────────────────────╯╰─────────────────────────────────────────╯│[p→pause/resume][x→cancel][r→retryfailed][d→deletequeued/done][c→clearfinished][b→browseuser][h/l→scrollname]││││││StatusInprogress││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que5.6/100.5MB·18.61MB/s·[█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]5%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger2.9 MB/52.5 MB (5%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger2.7MB/48.0MB(5%)8.9MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que11.2/100.5MB·18.61MB/s·[███░░░░░░░░░░░░░░░░░░░░░░░░░░░░]11%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger5.8 MB/52.5 MB (11%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger5.3MB/48.0MB(11%)8.9MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que16.8/100.5MB·18.61MB/s·[█████░░░░░░░░░░░░░░░░░░░░░░░░░░]16%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger8.8 MB/52.5 MB (16%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger8.0MB/48.0MB(16%)8.9MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que25.1/100.5MB·18.61MB/s·[███████░░░░░░░░░░░░░░░░░░░░░░░░]25%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger13.1 MB/52.5 MB (25%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger12.0MB/48.0MB(25%)8.9MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que30.7/100.5MB·18.61MB/s·[█████████░░░░░░░░░░░░░░░░░░░░░░]30%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger16.0 MB/52.5 MB (30%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger14.7MB/48.0MB(30%)8.9MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que36.3/100.5MB·18.61MB/s·[███████████░░░░░░░░░░░░░░░░░░░░]36%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger19.0 MB/52.5 MB (36%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger17.3MB/48.0MB(36%)8.9MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que41.9/100.5MB·18.61MB/s·[████████████░░░░░░░░░░░░░░░░░░░]41%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger21.9 MB/52.5 MB (41%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger20.0MB/48.0MB(41%)8.9MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer5.6MB/41.0MB(13%)3.7MB/s│││╭[3]Downloads/Uploads─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│StatusFilenameUserProgressSpeed││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flloopdigger21.9 MB/52.5 MB (41%)9.7 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(loopdigger20.0MB/48.0MB(41%)8.9MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer6.0MB/41.0MB(14%)3.7MB/s││[Tab/1-3→pane][z→unzoom][w→hide][?→keys][q→quit]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que44.7/100.5MB·18.61MB/s·[█████████████░░░░░░░░░░░░░░░░░░]44%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flloopdigger23.3 MB/52.5 MB (44%)9.7 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(loopdigger21.3MB/48.0MB(44%)8.9MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que50.2/100.5MB·18.61MB/s·[███████████████░░░░░░░░░░░░░░░░]50%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flloopdigger26.2 MB/52.5 MB (50%)9.7 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(loopdigger24.0MB/48.0MB(50%)8.9MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que55.8/100.5MB·18.61MB/s·[█████████████████░░░░░░░░░░░░░░]55%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flloopdigger29.2 MB/52.5 MB (55%)9.7 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(loopdigger26.7MB/48.0MB(55%)8.9MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que61.4/100.5MB·18.61MB/s·[██████████████████░░░░░░░░░░░░░]61%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flloopdigger32.1 MB/52.5 MB (61%)9.7 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(loopdigger29.3MB/48.0MB(61%)8.9MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que67.0/100.5MB·18.61MB/s·[████████████████████░░░░░░░░░░░]66%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flloopdigger35.0 MB/52.5 MB (66%)9.7 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(loopdigger32.0MB/48.0MB(66%)8.9MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que72.6/100.5MB·18.61MB/s·[██████████████████████░░░░░░░░░]72%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flloopdigger37.9 MB/52.5 MB (72%)9.7 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(loopdigger34.7MB/48.0MB(72%)8.9MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que78.2/100.5MB·18.61MB/s·[████████████████████████░░░░░░░]77%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flloopdigger40.8 MB/52.5 MB (77%)9.7 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(loopdigger37.3MB/48.0MB(77%)8.9MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(loopdigger38.7MB/48.0MB(80%)8.9MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer13.6MB/41.0MB(33%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que83.8/100.5MB·18.61MB/s·[█████████████████████████░░░░░░]83%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger43.8 MB/52.5 MB (83%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger40.0MB/48.0MB(83%)8.9MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer14.0MB/41.0MB(34%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que89.3/100.5MB·18.61MB/s·[███████████████████████████░░░░]88%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger46.7 MB/52.5 MB (88%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger42.7MB/48.0MB(88%)8.9MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que94.9/100.5MB·18.61MB/s·[█████████████████████████████░░]94%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger49.6 MB/52.5 MB (94%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger45.3MB/48.0MB(94%)8.9MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que97.7/100.5MB·18.61MB/s·[██████████████████████████████░]97%││StatusQueryResults││StatusFilenameUserProgressSpeed││05-HarbourBells.flac││›Donepublic domain fi15││›↯@@gufoo\Floopdigger51.0 MB/52.5 MB (97%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger46.7MB/48.0MB(97%)8.9MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer17.0MB/41.0MB(41%)3.7MB/s││││││││Userloopdigger││││││Size52.5MB││││││StatusInprogress││││││Saveto││││││/home/demo/Music/Soulseek││││││Bitrate902kbps││││││Length9m33s││soulseek-rs🦀v14.1.2Downloads:0active,2completed,0failed,0que││›Donepublic domain fi15││›✓@@gufoo\FloopdiggerCompleted-││@@gufoo/FieldRecordings/CoastalDawn││││✓@@gufoo\FloopdiggerCompleted-││Sessions(2019)[FLAC24-96]││││││StatusCompleted││││↑↯HarbourBcrateminer19.9MB/41.0MB(48%)3.7MB/s│││╭[3]Downloads/Uploads─────────────────────────────────────────────────────────────────────╮╭[Info]──────────────────────────────────────────────╮│StatusFilenameUserProgressSpeed││05-HarbourBells.flac││›✓@@gufoo\Field Recordings\CoastalloopdiggerCompleted-││@@gufoo/FieldRecordings/CoastalDawnSessions││✓@@gufoo\FieldRecordings\CoastalloopdiggerCompleted-││(2019)[FLAC24-96]││↑↯HarbourBells(LiveattheQuay)crateminer20.3MB/41.0MB(49%)3.7MB/s││││││Userloopdigger││││Size52.5MB││││Bitrate902kbps││││Length9m33s││││││││Freeslots1available││││Uploadspeed3.4MB/s│╰────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────╯│↑↯HarbourBells(LiveattheQuay)crateminer27.6MB/41.0MB(67%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer29.2MB/41.0MB(71%)3.7MB/s││││[]…eldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\01-CoastalDawn(Sunr30.5MBloopdigger675kbps3.35MB/s1││[]…eldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\02-ReservoirWind.fla22.6MBloopdigger569kbps3.35MB/s1││[]…eldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(Lo48.0MBloopdigger920kbps3.35MB/s1││[]…eldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\04-TramDepot,06h.fl32.2MBloopdigger725kbps3.35MB/s1││›[ ]…eld Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flac52.5 MBloopdigger902 kbps3.35 MB/s1││[]…eldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\06-RainonZincRoof.30.4MBloopdigger804kbps3.35MB/s1││[]…less\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\01-EmptyC17.4MBtapehiss836kbps2.1MB/s2││[]…less\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\02-Forest38.0MBtapehiss741kbps2.1MB/s2││[]…less\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\03-Quarry34.6MBtapehiss753kbps2.1MB/s2││[]…less\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\04-NightF18.7MBtapehiss723kbps2.1MB/s2││[]…mplete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-BY)\01-9.4MBnectoUA320kbps1.05MB/s0││[]…mplete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-BY)\02-5.1MBnectoUA320kbps1.05MB/s0││[]…mplete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-BY)\03-19.5MBnectoUA320kbps1.05MB/s0││[]…mplete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-BY)\readm0.1MBnectoUA-1.05MB/s0││↑↯HarbourBells(LiveattheQuay)crateminer35.3MB/41.0MB(86%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer38.2MB/41.0MB(93%)3.7MB/s││││││↑↯HarbourBcrateminer38.6MB/41.0MB(94%)3.7MB/s││││││↑✓HarbourBcrateminerCompleted-││││[]@@gufoo\Field╭Keys───────────────────────────────────────────────────────────────────────────────────────────────────╮3.35MB/s1││[]@@gufoo\Field│PanesSearches│3.35MB/s1││›[ ]@@gufoo\Field│Tab/Shift-Tabnext/previouspaneEntershowitsresults│3.35 MB/s1││[]@@gufoo\Field│123focusapane,hiddenornotdremovethesearch│3.35MB/s1││[]@@gufoo\Field│zzoomthefocusedpaneCcleareverysearch│3.35MB/s1││[]Music\lossless│whidethefocusedpanecchatrooms│2.1MB/s2││[]Music\lossless│Escleavezoom│2.1MB/s2││[]Music\lossless│clickfocusapaneResults│2.1MB/s2││[]Music\lossless│Spaceselect/deselect│2.1MB/s2││[]@@hnttf\comple│Anywherea/Aselectall/none│1.05MB/s0││[]@@hnttf\comple│ssearchthenetworkEnterdownloadtheselection│1.05MB/s0││[]@@hnttf\comple│bbrowseauser'sfiles/filter,Enterkeepsit,Esccle│1.05MB/s0││[]@@hnttf\comple│mcomposeaprivatemessagebbrowsetheowner│1.05MB/s0│││iinboxcchatrooms││││osettings││││?thislistDownloads/Uploads││││qquitppause/resume││╰────────────────────│xcancel│─────────────────────╯╭[1]Searches──────│Anylistrretryafailedone│─────────────────────╮│StatusQuery│↑↓/kjmoveonerowddeleteaqueuedorfinishedone│flac││›Donepublic doma│HomeEnd/gGfirst/lastrowccleareveryfinishedone│dings/CoastalDawn│││PgUpPgDn/^b^fonepage│AC24-96]│││^u^dhalfapageChatandbrowsepopups││││hl/←→scrollalongnamesidewaysPgUpPgDn/^b^fapageofmessages,orofthet│loopdigger│││0/$itsstart/itsend^u^dhalfapageofthem│52.5MB│││HomeEnd/gGoldest/newest,orfirst/las│Completed│││Tab/Shift-Tabnextroom,chatoruser│││╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ulseek││[?/Esc→close]│╰────────────────────────────────────────────────────────────────╭[1]Searches────────────────────╮╭[3]Downloads/Uploads───│││││╭Shortcuts·Sharing:~/Music───────────────────────────────────────────────────────────────────────────│[s→search][Enter→results][d→remove][m→message][i→inbox][c→chat][b→browseuser][h/l→scrollquery╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│StatusQueryResults╮╭[3]Downloads/Uploads──────────────────────────────────────────────╮╭[Info]─────────────────────────────────╮╭│││││╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│[Type→searchterm][←→→movecursor][Backspace/Del→edit][Enter→search]││search:p││search:pu││search:pub││search:publ││search:publi││search:publicd││search:publicdo││search:publicdom││search:publicdoma││search:publicdomai││search:publicdomainf││search:publicdomainfi││search:publicdomainfie││search:publicdomainfiel││search:publicdomainfieldr││search:publicdomainfieldre││search:publicdomainfieldrec││search:publicdomainfieldreco││search:publicdomainfieldrecor││search:publicdomainfieldrecord││search:publicdomainfieldrecordi││search:publicdomainfieldrecordin││search:publicdomainfieldrecording│╰────────────s────────────────────╮╭[3]Downloads/Uploads──────────────────────────────────────────────╮╭[Info]─────────────────────────────────╮│StatusQueryResults│││││││││││╰──────────────────────────────────╯╰──────────────────────││[Space→select][Enter→download]───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮│[Tab/1-3→pane][z][←→→movecursor][Backspace/Del→edit][Enter→search][Esc→cancel]││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2││││││Length││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\041││›Activepublic domain fi11││││@@gufoo/FieldRecordings/CoastalDa││[]@@hnttf\complete\ExplorersoftheInterior-FieldRecordings2004(netlabel,CC-0│╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│[p→pause/resume][x→cancel][r→retryfailed][d→deletequeued/done][c[a/A→selectall/none][h/l→scrollname][g/G→top/end]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que2.8/100.5MB·18.61MB/s·[░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]2%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger1.5 MB/52.5 MB (2%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger1.3MB/48.0MB(2%)8.9MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que8.4/100.5MB·18.61MB/s·[██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░]8%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger4.4 MB/52.5 MB (8%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger4.0MB/48.0MB(8%)8.9MB/s││Sessions(2019)[FLAC24-96]││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que14.0/100.5MB·18.61MB/s·[████░░░░░░░░░░░░░░░░░░░░░░░░░░░]13%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger7.3 MB/52.5 MB (13%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger6.7MB/48.0MB(13%)8.9MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer0.2MB/41.0MB(0%)3.7MB/s││││││↑↯HarbourBcrateminer0.5MB/41.0MB(1%)3.7MB/s││││││↑↯HarbourBcrateminer0.9MB/41.0MB(2%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que19.5/100.5MB·18.61MB/s·[██████░░░░░░░░░░░░░░░░░░░░░░░░░]19%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger10.2 MB/52.5 MB (19%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger9.3MB/48.0MB(19%)8.9MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer1.3MB/41.0MB(3%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que22.3/100.5MB·18.61MB/s·[██████░░░░░░░░░░░░░░░░░░░░░░░░░]22%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger11.7 MB/52.5 MB (22%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger10.7MB/48.0MB(22%)8.9MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer1.7MB/41.0MB(4%)3.7MB/s││││││↑↯HarbourBcrateminer2.1MB/41.0MB(5%)3.7MB/s││││││↑↯HarbourBcrateminer2.5MB/41.0MB(6%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que27.9/100.5MB·18.61MB/s·[████████░░░░░░░░░░░░░░░░░░░░░░░]27%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger14.6 MB/52.5 MB (27%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger13.3MB/48.0MB(27%)8.9MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer2.9MB/41.0MB(7%)3.7MB/s││││││↑↯HarbourBcrateminer3.3MB/41.0MB(7%)3.7MB/s││││││↑↯HarbourBcrateminer3.6MB/41.0MB(8%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que33.5/100.5MB·18.61MB/s·[██████████░░░░░░░░░░░░░░░░░░░░░]33%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger17.5 MB/52.5 MB (33%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger16.0MB/48.0MB(33%)8.9MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer4.0MB/41.0MB(9%)3.7MB/s││││││↑↯HarbourBcrateminer4.4MB/41.0MB(10%)3.7MB/s││││││↑↯HarbourBcrateminer4.8MB/41.0MB(11%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que39.1/100.5MB·18.61MB/s·[████████████░░░░░░░░░░░░░░░░░░░]38%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger20.4 MB/52.5 MB (38%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger18.7MB/48.0MB(38%)8.9MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer5.2MB/41.0MB(12%)3.7MB/s││││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]-CoastalD30.5MBloopdigger675kbps3.35MB/s1││ieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\04-TramDepo32.2MBloopdigger725kbps3.35MB/s1││17.4MBtapehiss836kbps2.1MB/s2││oUA320kbps1.05MB/s0││───────────────────────────────────────────────────────╯│5.6MB/41.0MB(13%)3.7MB/s││││[Tab/1-3→pane][z→unzoom]→hide][?→keys][q→quit]││↑↯HarbourBells(LiveattheQuay).flaccrateminer6.4MB/41.0MB(15%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que47.5/100.5MB·18.61MB/s·[██████████████░░░░░░░░░░░░░░░░░]47%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flloopdigger24.8 MB/52.5 MB (47%)9.7 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(loopdigger22.7MB/48.0MB(47%)8.9MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer6.8MB/41.0MB(16%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer7.1MB/41.0MB(17%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer7.5MB/41.0MB(18%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que53.0/100.5MB·18.61MB/s·[████████████████░░░░░░░░░░░░░░░]52%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flloopdigger27.7 MB/52.5 MB (52%)9.7 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(loopdigger25.3MB/48.0MB(52%)8.9MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer7.9MB/41.0MB(19%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer8.3MB/41.0MB(20%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer8.7MB/41.0MB(21%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que58.6/100.5MB·18.61MB/s·[██████████████████░░░░░░░░░░░░░]58%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flloopdigger30.6 MB/52.5 MB (58%)9.7 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(loopdigger28.0MB/48.0MB(58%)8.9MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer9.0MB/41.0MB(22%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer9.4MB/41.0MB(22%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer9.8MB/41.0MB(23%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que64.2/100.5MB·18.61MB/s·[███████████████████░░░░░░░░░░░░]63%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flloopdigger33.5 MB/52.5 MB (63%)9.7 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(loopdigger30.7MB/48.0MB(63%)8.9MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer10.2MB/41.0MB(24%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer10.6MB/41.0MB(25%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer11.0MB/41.0MB(26%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que69.8/100.5MB·18.61MB/s·[█████████████████████░░░░░░░░░░]69%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flloopdigger36.5 MB/52.5 MB (69%)9.7 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(loopdigger33.3MB/48.0MB(69%)8.9MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer11.3MB/41.0MB(27%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer11.7MB/41.0MB(28%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer12.1MB/41.0MB(29%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que75.4/100.5MB·18.61MB/s·[███████████████████████░░░░░░░░]75%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flloopdigger39.4 MB/52.5 MB (75%)9.7 MB/s││↯@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\03-MarshatNight(loopdigger36.0MB/48.0MB(75%)8.9MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer12.5MB/41.0MB(30%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer12.9MB/41.0MB(31%)3.7MB/s││↑↯HarbourBells(LiveattheQuay).flaccrateminer13.2MB/41.0MB(32%)3.7MB/s││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que81.0/100.5MB·18.61MB/s·[████████████████████████░░░░░░░]80%││›↯@@gufoo\Field Recordings\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flloopdigger42.3 MB/52.5 MB (80%)9.7 MB/s││[]@@gufoo\FieldRecordings\gs\Coastal Dawn Sessions (2019) [FLAC 24-96]\05 - Harbour Bells.flloopdigger42.3 MB/52.5 MB (80%)9.7 MB/s││[]@@gufoo\FieldRecordings\CoastalDawnSessions(2019)[FLAC24-96]\06-RainonZ││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0238.0MBtapehiss741kbps││[]@@hnttf\compl│╰──────────────────────────────││StatusQueryResults││││││[Tab/1-3→pane][z→zoom][w[w→hide][?→keys][q→quit]││││↑↯HarbourBcrateminer14.2MB/41.0MB(34%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que86.5/100.5MB·18.61MB/s·[██████████████████████████░░░░░]86%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger45.2 MB/52.5 MB (86%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger41.3MB/48.0MB(86%)8.9MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer14.6MB/41.0MB(35%)3.7MB/s││││││↑↯HarbourBcrateminer15.0MB/41.0MB(36%)3.7MB/s││││││↑↯HarbourBcrateminer15.4MB/41.0MB(37%)3.7MB/s││││soulseek-rs🦀v14.1.2Downloads:2active,0completed,0failed,0que92.1/100.5MB·18.61MB/s·[████████████████████████████░░░]91%││›Donepublic domain fi15││›↯@@gufoo\Floopdigger48.1 MB/52.5 MB (91%)9.7 MB/s││@@gufoo/FieldRecordings/CoastalDawn││││↯@@gufoo\Floopdigger44.0MB/48.0MB(91%)8.9MB/s││Sessions(2019)[FLAC24-96]││││↑↯HarbourBcrateminer15.8MB/41.0MB(38%)3.7MB/s││││││↑↯HarbourBcrateminer16.2MB/41.0MB(39%)3.7MB/s││││││↑↯HarbourBcrateminer16.6MB/41.0MB(40%)3.7MB/s││││││↑↯HarbourBcrateminer16.6MB/41.0MB(40%)3.7MB/s│││╰──────────────────────────────────╯╰─────────────────────────────────────────────────────────────────────╯╰─────────────────────────────────────────╯│[s→search][Enter→results][d→remove][m→message][i→inbox]done][c→clearfinished][b→browseuser][h/l→scrollname]││[z→zoom][w→hide][?→keys][q→quit→keys][q→quit]││││↑↯HarbourBcrateminer17.2MB/41.0MB(41%)3.7MB/s││││││↑↯HarbourBcrateminer17.6MB/41.0MB(42%)3.7MB/s││││││↑↯HarbourBcrateminer18.0MB/41.0MB(43%)3.7MB/s││││││↑↯HarbourBcrateminer18.4MB/41.0MB(44%)3.7MB/s││││││↑↯HarbourBcrateminer18.8MB/41.0MB(45%)3.7MB/s││││││↑↯HarbourBcrateminer19.2MB/41.0MB(46%)3.7MB/s││││││↑↯HarbourBcrateminer19.6MB/41.0MB(47%)3.7MB/s│││╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯│StatusFilenameUserProgressSpeed││05d││05-HarbourBells.flac││✓@@gufoo\FieldRecordings\CoastalloopdiggerCompleted-││(2019)[FLAC24-96]ns(2019)[FLAC24-96]││││StatusCompleted││││Uploadspeed9m33s││[Space→select][Enter→download][b→browseowner][c[i→inbox][c→chat][b→browseuser][h/l→scrollquery][Tab/1-3→pane]││[Tab/1-3→pane][z→zoom][w→hide][it]││↑↯HarbourBells(LiveattheQuay)crateminer20.6MB/41.0MB(50%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer21.0MB/41.0MB(51%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer21.4MB/41.0MB(52%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer21.8MB/41.0MB(53%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer22.1MB/41.0MB(53%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer22.5MB/41.0MB(54%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer22.9MB/41.0MB(55%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer23.3MB/41.0MB(56%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer23.7MB/41.0MB(57%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer24.1MB/41.0MB(58%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer24.5MB/41.0MB(59%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer24.9MB/41.0MB(60%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer25.3MB/41.0MB(61%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer25.6MB/41.0MB(62%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer26.0MB/41.0MB(63%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer26.4MB/41.0MB(64%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer26.8MB/41.0MB(65%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer27.2MB/41.0MB(66%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer28.0MB/41.0MB(68%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer28.4MB/41.0MB(69%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer28.8MB/41.0MB(70%)3.7MB/s││││[]…less\PublicDomainFieldRecordings,Vol.2(CreativeCreativeCommonsZero)\0334.6MBtapehiss753kbps2.1MB/s2││↑↯HarbourBells(LiveattheQuay)crateminer29.5MB/41.0MB(72%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer29.9MB/41.0MB(72%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer30.2MB/41.0MB(73%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer30.6MB/41.0MB(74%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer31.0MB/41.0MB(75%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer31.4MB/41.0MB(76%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer31.8MB/41.0MB(77%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer32.2MB/41.0MB(78%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer32.6MB/41.0MB(79%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer33.0MB/41.0MB(80%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer33.4MB/41.0MB(81%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer33.7MB/41.0MB(82%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer34.1MB/41.0MB(83%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer34.5MB/41.0MB(84%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer34.9MB/41.0MB(85%)3.7MB/s││││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommons-Quarry34.6MBtapehiss753kbps2.1MB/s2││↑↯HarbourBells(LiveattheQuay)crateminer35.7MB/41.0MB(87%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer35.8MB/41.0MB(87%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer36.2MB/41.0MB(88%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer36.6MB/41.0MB(89%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer37.0MB/41.0MB(90%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer37.4MB/41.0MB(91%)3.7MB/s││││↑↯HarbourBells(LiveattheQuay)crateminer37.8MB/41.0MB(92%)3.7MB/s││││StatusQueryResults││StatusFilenameUserSpeed││05-HarbourBells.flac││✓@@gufoo\FieldRecordings\CoastalloopdiggerCompleted-││(2019)[FLAC24-96]││││││User││[s→e→select][Enter→download][b→browseowner][c→chat][/→filter][a/A→selectall/none][h/l→scrollname][g/G→top/end]││[s→search][Enter→results][d→remove][m→message][i→inbox][c→chat][b→browseuser][h/l→scrollquery][][g/G→top/end]││││↑↯HarbourBcrateminer38.8MB/41.0MB(94%)3.7MB/s││││││↑↯HarbourBcrateminer39.2MB/41.0MB(95%)3.7MB/s││││││↑↯HarbourBcrateminer39.6MB/41.0MB(96%)3.7MB/s││││││↑↯HarbourBcrateminer40.0MB/41.0MB(97%)3.7MB/s││││││↑↯HarbourBcrateminer40.4MB/41.0MB(98%)3.7MB/s││││││↑↯HarbourBcrateminer40.7MB/41.0MB(99%)3.7MB/s││││[]Music\lossless│clickfocusapaneResults34.6MBtapehiss753kbps2.1MB/s2││[]@@hnttf\comple│bbrowseauser'sfiles/filter,Enter320kbps1.05MB/s0│││qquit─────────────────────────────────────────────────────────────────────────────────╯│Statpublic domain fi15││›✓@@gufoo\FloopdiggerCompleted-││@@gufoo/FieldRecordings/CoastalDawn│││PgUpPgDn/^b^fonepage-│││││││││Saveto│││╰─────────────────────────────────────────────────────────────────────╯╰─────────────────────────────────────────╯│[]@@gufoo\FieldRecordingszoomthefocusedpaneCcleareverysearch│3.35MB/s1││[]Music\lossless\PublicDomainFieldRecordings,Vol.2(CreativeCommonsZero)\0418.7MB/deselect│2.1MB/s2│╭[1]Searches────────────────────╮╭[3]Downloads/Uploads───────────────────cancel│─────────────────────╯│›Donepublic domain fi15││›moveonerowddeleteaqueuedorfinishedone│flac││││↑✓HarbourB│AC24-96]││HomeEnd/gGoldest/newest,orfirst/las│Completed│╰──────────────────────────────────╯╰───────────────────────────────││││[s→search][Enter→results][d→remove][m→message][i→inbox]─────────────────────────────────────────────────────────────────────────────╮│[z→zoom][w→hide][?→keys][q→quit│ \ No newline at end of file diff --git a/soulseek-rs/src/models/app_state.rs b/soulseek-rs/src/models/app_state.rs index c0b6061..0030668 100644 --- a/soulseek-rs/src/models/app_state.rs +++ b/soulseek-rs/src/models/app_state.rs @@ -108,6 +108,75 @@ impl PaneLayout { } } +/// Where a scrolling log is being read. +/// +/// Either following its newest line, or held at a row of its history so +/// that arriving messages do not push the reader along. The renderer records +/// the log's size at each draw, which is what lets the keys move by a +/// screenful and stop at the ends. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct LogView { + /// The first visible row while held in the history; `None` follows the + /// tail. + top: Option, + /// Rows the log had, and rows the window showed, at the last draw. + rows: usize, + height: usize, +} + +impl LogView { + /// Whether the newest line is on screen, so new messages are being seen. + #[must_use] + pub const fn following(&self) -> bool { + self.top.is_none() + } + + /// Rows a page key moves: what the window showed last time. + #[must_use] + pub const fn page(&self) -> usize { + self.height + } + + const fn last_top(&self) -> usize { + self.rows.saturating_sub(self.height) + } + + /// The rows of a log `len` long to draw in a window `height` tall. A + /// held position that has reached the tail goes back to following it. + pub fn window( + &mut self, + len: usize, + height: usize, + ) -> std::ops::Range { + self.rows = len; + self.height = height.max(1); + let top = match self.top { + Some(top) if top < self.last_top() => top, + _ => { + self.top = None; + self.last_top() + } + }; + top..(top + self.height).min(len) + } + + /// Move the window `rows` down the log (up, when negative), holding + /// wherever it lands unless that is the tail. + pub fn scroll(&mut self, rows: isize) { + let current = self.top.unwrap_or_else(|| self.last_top()); + let next = current.saturating_add_signed(rows); + self.top = (next < self.last_top()).then_some(next); + } + + pub const fn to_oldest(&mut self) { + self.top = Some(0); + } + + pub const fn to_newest(&mut self) { + self.top = None; + } +} + /// What the shared command bar is currently capturing input for. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CommandBarMode { @@ -139,6 +208,8 @@ pub struct AppState { pub searches: Vec, pub searches_table_state: TableState, pub selected_search_index: Option, + /// Cells the query column is scrolled to the left. + pub searches_query_offset: usize, // Results pub results_items: Vec, @@ -148,11 +219,14 @@ pub struct AppState { pub results_selected_indices: std::collections::HashSet, pub results_filter_query: String, pub results_is_filtering: bool, + /// Cells the file-name column is scrolled to the left. pub results_name_offset: usize, // Downloads pub downloads: Vec, pub downloads_table_state: TableState, + /// Cells the transfers' file-name column is scrolled to the left. + pub downloads_name_offset: usize, pub downloads_receiver_channel: Option)>>, pub downloads_sender_channel: @@ -183,6 +257,8 @@ pub struct AppState { /// Compose buffer for the active conversation. pub chat_input: String, pub chat_composing: bool, + /// Where the conversation is being read. + pub chat_view: LogView, // Browse users' shared files (one tab per user) pub browse: BrowseTabs, @@ -204,6 +280,9 @@ pub struct AppState { pub searches_pane_area: Option, pub results_pane_area: Option, pub downloads_pane_area: Option, + /// Where the open popup was last drawn, for its page size. Only one is + /// ever open, so one record serves them all. + pub popup_area: Option, } impl AppState { @@ -222,6 +301,7 @@ impl AppState { searches: Vec::new(), searches_table_state, selected_search_index: None, + searches_query_offset: 0, results_items: Vec::new(), results_filtered_items: Vec::new(), @@ -234,6 +314,7 @@ impl AppState { downloads: Vec::new(), downloads_table_state, + downloads_name_offset: 0, downloads_receiver_channel: None, downloads_sender_channel: None, active_downloads_count: 0, @@ -254,6 +335,7 @@ impl AppState { chat_peer: None, chat_input: String::new(), chat_composing: false, + chat_view: LogView::default(), browse: BrowseTabs::new(), show_browse: false, @@ -270,6 +352,16 @@ impl AppState { searches_pane_area: None, results_pane_area: None, downloads_pane_area: None, + popup_area: None, + } + } + + /// How far the focused list's long column is scrolled sideways. + pub const fn name_offset_mut(&mut self, pane: FocusedPane) -> &mut usize { + match pane { + FocusedPane::Searches => &mut self.searches_query_offset, + FocusedPane::Results => &mut self.results_name_offset, + FocusedPane::Downloads => &mut self.downloads_name_offset, } } @@ -302,6 +394,7 @@ impl AppState { pub fn open_chat(&mut self, peer: String) { self.chat_peer = Some(peer); self.chat_input.clear(); + self.chat_view.to_newest(); self.chat_composing = true; self.show_messages = true; self.unread_messages = 0; @@ -351,6 +444,10 @@ impl AppState { (current + peers.len() - 1) % peers.len() }; let peer = peers[next].to_string(); + // A different conversation opens at its end; the same one stays put. + if next != current { + self.chat_view.to_newest(); + } self.chat_peer = Some(peer); } } @@ -486,6 +583,55 @@ mod tests { assert!(!state.layout.is_visible(FocusedPane::Searches)); } + #[test] + fn a_log_view_follows_the_tail_until_held_and_holds_against_new_rows() { + let mut view = LogView::default(); + assert_eq!(view.window(100, 10), 90..100); + assert!(view.following()); + + view.scroll(-10); + assert_eq!(view.window(100, 10), 80..90); + assert!(!view.following()); + + // Five more rows arrive: the held window does not move. + assert_eq!(view.window(105, 10), 80..90); + + // Back down to the tail resumes following, including new rows. + view.scroll(10); + view.scroll(10); + assert_eq!(view.window(105, 10), 95..105); + assert!(view.following()); + + view.to_oldest(); + assert_eq!(view.window(105, 10), 0..10); + view.scroll(-10); + assert_eq!(view.window(105, 10), 0..10, "clamped at the top"); + view.to_newest(); + assert_eq!(view.window(105, 10), 95..105); + } + + #[test] + fn a_log_shorter_than_its_window_is_never_held() { + let mut view = LogView::default(); + assert_eq!(view.window(3, 10), 0..3); + view.to_oldest(); + assert_eq!(view.window(3, 10), 0..3); + assert!(view.following(), "nothing to hold"); + } + + #[test] + fn cycling_to_the_same_chat_keeps_the_place() { + let mut state = AppState::new(); + say(&mut state, "alice"); + state.chat_view.window(50, 10); + state.chat_view.scroll(-10); + state.cycle_chat_peer(true); + assert!(!state.chat_view.following(), "only one chat: nothing moved"); + say(&mut state, "bob"); + state.cycle_chat_peer(true); + assert!(state.chat_view.following(), "a new chat opens at its end"); + } + #[test] fn opening_a_chat_pins_a_peer_with_no_messages_yet() { let mut state = AppState::new(); diff --git a/soulseek-rs/src/models/mod.rs b/soulseek-rs/src/models/mod.rs index 554639d..bbe1a7b 100644 --- a/soulseek-rs/src/models/mod.rs +++ b/soulseek-rs/src/models/mod.rs @@ -5,7 +5,7 @@ mod rooms; mod settings; pub use app_state::{ - AppState, ChatMessage, CommandBarMode, DownloadEntry, FocusedPane, + AppState, ChatMessage, CommandBarMode, DownloadEntry, FocusedPane, LogView, MessageDirection, PaneLayout, SearchEntry, SearchStatus, }; pub use browse::{ diff --git a/soulseek-rs/src/models/rooms.rs b/soulseek-rs/src/models/rooms.rs index f3cee71..b4f3db1 100644 --- a/soulseek-rs/src/models/rooms.rs +++ b/soulseek-rs/src/models/rooms.rs @@ -1,3 +1,4 @@ +use crate::models::LogView; use chrono::{DateTime, Local}; use soulseek_rs::types::{RoomEvent, RoomInfo}; @@ -38,6 +39,8 @@ pub struct OpenRoom { pub unread: usize, /// Per-room compose buffer. pub input: String, + /// Where the log is being read. + pub view: LogView, } impl OpenRoom { @@ -160,6 +163,11 @@ impl RoomsState { .and_then(|r| r.users.get(self.user_selected).cloned()) } + /// Where the active room's log is being read, to move it. + pub fn active_view_mut(&mut self) -> Option<&mut LogView> { + self.open.get_mut(self.active).map(|room| &mut room.view) + } + pub const fn select_user_up(&mut self) { self.user_selected = self.user_selected.saturating_sub(1); } diff --git a/soulseek-rs/src/ui/main_tui/browse.rs b/soulseek-rs/src/ui/main_tui/browse.rs index c3cbb23..525a063 100644 --- a/soulseek-rs/src/ui/main_tui/browse.rs +++ b/soulseek-rs/src/ui/main_tui/browse.rs @@ -1,6 +1,7 @@ use super::MainTui; +use super::input::{jumped, list_jump}; use crate::models::{BrowseStatus, files_under, find_node}; -use ratatui::crossterm::event::{KeyCode, KeyEvent}; +use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use std::{thread, time::Duration}; /// How long to wait for a browse response before showing a timeout notice. @@ -12,6 +13,11 @@ impl MainTui { self.state.show_browse = false; return; } + let jump = list_jump(key, self.popup_page()); + // Control combinations are only ever jumps: ctrl-d pages, d downloads. + if jump.is_none() && key.modifiers.contains(KeyModifiers::CONTROL) { + return; + } // Tab management: switch between browsed users or close the active tab. match key.code { @@ -60,6 +66,14 @@ impl MainTui { (rows, sel, row) }; + if let Some(jump) = jump { + if let Some(browse) = self.state.browse.active_tab_mut() { + browse.selected_row = jumped(sel, rows.len(), jump); + } + self.sync_browse_selection(); + return; + } + // Downloads need `&self.client` free of the browse borrow. match key.code { KeyCode::Enter if !row.is_folder => { diff --git a/soulseek-rs/src/ui/main_tui/input.rs b/soulseek-rs/src/ui/main_tui/input.rs index 4d7871d..2baa211 100644 --- a/soulseek-rs/src/ui/main_tui/input.rs +++ b/soulseek-rs/src/ui/main_tui/input.rs @@ -1,7 +1,10 @@ use super::MainTui; -use crate::models::{CommandBarMode, FocusedPane}; +use crate::models::{CommandBarMode, FocusedPane, LogView}; use crate::ui::pane_block; -use crate::ui::panes::name_end_offset; +use crate::ui::panes::{ + InfoSubject, name_end_offset, query_end_offset, selected_transfer, + transfer_name_end_offset, upload_display_name, +}; use ratatui::crossterm::event::{ KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind, }; @@ -195,6 +198,14 @@ impl MainTui { return; } + // Paging keys move through the conversation's history. + if scroll_log(&mut self.state.chat_view, key) { + return; + } + if key.modifiers.contains(KeyModifiers::CONTROL) { + return; + } + match key.code { KeyCode::Char('i' | 'q') | KeyCode::Esc => { self.state.show_messages = false; @@ -375,6 +386,15 @@ impl MainTui { navigate_list(key, table, len, page) } + /// Rows a popup shows at once: its inner height less a tab bar and a + /// compose line, which is what its list or log has left. + pub(super) fn popup_page(&self) -> isize { + let rows = self.state.popup_area.map_or(1, |area| { + pane_block(false).inner(area).height.saturating_sub(2) + }); + isize::try_from(rows.max(1)).unwrap_or(isize::MAX) + } + /// Rows a pane shows at once: its inner height less the table header. fn page_size(&self, pane: FocusedPane) -> isize { let area = match pane { @@ -389,7 +409,7 @@ impl MainTui { } fn handle_searches_input(&mut self, key: KeyEvent) { - if self.navigate_focused_list(key) { + if self.navigate_focused_list(key) || self.scroll_focused_name(key) { return; } match key.code { @@ -426,20 +446,10 @@ impl MainTui { } fn handle_results_input(&mut self, key: KeyEvent) { - if self.navigate_focused_list(key) { + if self.navigate_focused_list(key) || self.scroll_focused_name(key) { return; } match key.code { - KeyCode::Right | KeyCode::Char('l') => { - self.scroll_names(NAME_SCROLL_STEP); - } - KeyCode::Left | KeyCode::Char('h') => { - self.scroll_names(-NAME_SCROLL_STEP); - } - KeyCode::Char('0') => self.state.results_name_offset = 0, - KeyCode::Char('$') => { - self.state.results_name_offset = self.highlighted_name_end(); - } KeyCode::Char(' ') => { let index = self .state @@ -475,24 +485,82 @@ impl MainTui { } } - fn scroll_names(&mut self, delta: isize) { - let end = self.highlighted_name_end(); - let current = self.state.results_name_offset.min(end); - self.state.results_name_offset = - current.saturating_add_signed(delta).min(end); + /// The keys that scroll the focused list's long column sideways, the + /// same in every list: a file name in Results and Downloads, the query + /// in Searches. Says whether `key` was one of them. + fn scroll_focused_name(&mut self, key: KeyEvent) -> bool { + match key.code { + KeyCode::Right | KeyCode::Char('l') => { + self.scroll_names(NAME_SCROLL_STEP); + } + KeyCode::Left | KeyCode::Char('h') => { + self.scroll_names(-NAME_SCROLL_STEP); + } + KeyCode::Char('0') => { + *self.state.name_offset_mut(self.state.focused_pane) = 0; + } + KeyCode::Char('$') => { + let end = self.highlighted_name_end(); + *self.state.name_offset_mut(self.state.focused_pane) = end; + } + _ => return false, + } + true } + fn scroll_names(&mut self, delta: isize) { + let end = self.highlighted_name_end(); + let offset = self.state.name_offset_mut(self.state.focused_pane); + let current = (*offset).min(end); + *offset = current.saturating_add_signed(delta).min(end); + } + + /// How far the highlighted row's long column can scroll before its end + /// is in view. Zero when nothing is highlighted or the pane is not drawn. fn highlighted_name_end(&self) -> usize { - let Some(area) = self.state.results_pane_area else { - return 0; - }; - self.highlighted_result() - .map_or(0, |file| name_end_offset(&file.filename, area)) + match self.state.focused_pane { + FocusedPane::Searches => { + let Some(area) = self.state.searches_pane_area else { + return 0; + }; + self.state + .searches_table_state + .selected() + .and_then(|row| self.state.searches.get(row)) + .map_or(0, |search| query_end_offset(&search.query, area)) + } + FocusedPane::Results => { + let Some(area) = self.state.results_pane_area else { + return 0; + }; + self.highlighted_result() + .map_or(0, |file| name_end_offset(&file.filename, area)) + } + FocusedPane::Downloads => { + let Some(area) = self.state.downloads_pane_area else { + return 0; + }; + let shown = match selected_transfer( + self.state.downloads_table_state.selected(), + &self.state.downloads, + &self.state.uploads, + ) { + Some(InfoSubject::Download(entry)) => { + entry.download.filename.as_str() + } + Some(InfoSubject::Upload(upload)) => { + upload_display_name(upload) + } + Some(InfoSubject::Result(_)) | None => return 0, + }; + transfer_name_end_offset(shown, area) + } + } } fn handle_downloads_input(&mut self, key: KeyEvent) { // The pane lists downloads first, then uploads; navigation spans both. - if self.navigate_focused_list(key) { + if self.navigate_focused_list(key) || self.scroll_focused_name(key) { return; } match key.code { @@ -539,9 +607,58 @@ impl MainTui { } } -/// The keys every list answers alike: a row at a time wrapping at either -/// end, a page or half a page at a time stopping there, and the two ends. +/// A move over a whole list at once: to either end, or by a number of rows +/// (negative is up). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ListJump { + First, + Last, + Rows(isize), +} + +/// The keys every list and log answers alike for moving further than a row: +/// the two ends, a page, half a page. `page` is how many rows the list shows. +pub(super) fn list_jump(key: KeyEvent, page: isize) -> Option { + let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); + let half = (page / 2).max(1); + Some(match key.code { + KeyCode::Home | KeyCode::Char('g') if !ctrl => ListJump::First, + KeyCode::End | KeyCode::Char('G') if !ctrl => ListJump::Last, + KeyCode::PageDown => ListJump::Rows(page), + KeyCode::PageUp => ListJump::Rows(-page), + KeyCode::Char('f') if ctrl => ListJump::Rows(page), + KeyCode::Char('b') if ctrl => ListJump::Rows(-page), + KeyCode::Char('d') if ctrl => ListJump::Rows(half), + KeyCode::Char('u') if ctrl => ListJump::Rows(-half), + _ => return None, + }) +} + +/// Apply the jump keys to a log, moving by the screenful it last showed. /// Says whether `key` was one of them. +pub(super) fn scroll_log(view: &mut LogView, key: KeyEvent) -> bool { + let page = isize::try_from(view.page()).unwrap_or(isize::MAX); + match list_jump(key, page) { + Some(ListJump::First) => view.to_oldest(), + Some(ListJump::Last) => view.to_newest(), + Some(ListJump::Rows(rows)) => view.scroll(rows), + None => return false, + } + true +} + +/// `selected` moved by `jump` over a list `len` long, stopping at the ends. +pub(super) fn jumped(selected: usize, len: usize, jump: ListJump) -> usize { + let last = len.saturating_sub(1); + match jump { + ListJump::First => 0, + ListJump::Last => last, + ListJump::Rows(rows) => selected.saturating_add_signed(rows).min(last), + } +} + +/// The keys every list answers alike: a row at a time wrapping at either +/// end, and the jumps of [`list_jump`]. Says whether `key` was one of them. fn navigate_list( key: KeyEvent, table: &mut TableState, @@ -549,23 +666,16 @@ fn navigate_list( page: isize, ) -> bool { let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); - let half = (page / 2).max(1); match key.code { KeyCode::Up | KeyCode::Char('k') if !ctrl => cycle(table, len, false), KeyCode::Down | KeyCode::Char('j') if !ctrl => cycle(table, len, true), - KeyCode::Home | KeyCode::Char('g') if !ctrl => { - select_row(table, len, 0); - } - KeyCode::End | KeyCode::Char('G') if !ctrl => { - select_row(table, len, len.saturating_sub(1)); - } - KeyCode::PageDown => scroll_rows(table, len, page), - KeyCode::PageUp => scroll_rows(table, len, -page), - KeyCode::Char('f') if ctrl => scroll_rows(table, len, page), - KeyCode::Char('b') if ctrl => scroll_rows(table, len, -page), - KeyCode::Char('d') if ctrl => scroll_rows(table, len, half), - KeyCode::Char('u') if ctrl => scroll_rows(table, len, -half), - _ => return false, + _ => match list_jump(key, page) { + Some(jump) => { + let current = table.selected().unwrap_or(0); + select_row(table, len, jumped(current, len, jump)); + } + None => return false, + }, } true } @@ -590,9 +700,3 @@ fn select_row(table: &mut TableState, len: usize, target: usize) { table.select(Some(target.min(len - 1))); } } - -/// Move the selection `delta` rows, stopping at either end. -fn scroll_rows(table: &mut TableState, len: usize, delta: isize) { - let current = table.selected().unwrap_or(0); - select_row(table, len, current.saturating_add_signed(delta)); -} diff --git a/soulseek-rs/src/ui/main_tui/mod.rs b/soulseek-rs/src/ui/main_tui/mod.rs index 65de6ad..bfffd32 100644 --- a/soulseek-rs/src/ui/main_tui/mod.rs +++ b/soulseek-rs/src/ui/main_tui/mod.rs @@ -1548,12 +1548,12 @@ mod tests { let panes = row_with(&screen, "Panes"); assert!(!panes.contains("Searches"), "one column: {panes}"); assert!(screen.contains("focus a pane, hidden or not"), "{screen}"); - assert!(!screen.contains("clear every finished one"), "{screen}"); + assert!(!screen.contains("next room, chat or user"), "{screen}"); press(&mut tui, KeyCode::End); let screen = screen_sized(&mut tui, 80, 24); - assert!(screen.contains("clear every finished one"), "{screen}"); - assert!(!screen.contains("Tab / Shift-Tab"), "{screen}"); + assert!(screen.contains("next room, chat or user"), "{screen}"); + assert!(!screen.contains("next / previous pane"), "{screen}"); press(&mut tui, KeyCode::Home); let screen = screen_sized(&mut tui, 80, 24); @@ -1587,6 +1587,208 @@ mod tests { assert_eq!(tui.state.focused_pane, FocusedPane::Results); } + #[test] + fn l_and_h_scroll_a_transfer_name_and_a_query_too() { + let mut tui = furnished_tui(); + tui.state.downloads[0].download.filename = "y".repeat(120); + tui.state.searches[0].query = "z".repeat(60); + let _ = screen_of(&mut tui); + + tui.state.focused_pane = FocusedPane::Downloads; + press(&mut tui, KeyCode::Char('l')); + assert_eq!(tui.state.downloads_name_offset, 8); + press(&mut tui, KeyCode::Char('$')); + let end = tui.state.downloads_name_offset; + assert!(end > 8, "the end is past a step: {end}"); + let screen = screen_of(&mut tui); + assert!(row_with(&screen, "…yyyy").contains("…yyyy"), "{screen}"); + press(&mut tui, KeyCode::Char('0')); + assert_eq!(tui.state.downloads_name_offset, 0); + + tui.state.focused_pane = FocusedPane::Searches; + press(&mut tui, KeyCode::Right); + assert_eq!(tui.state.searches_query_offset, 8); + press(&mut tui, KeyCode::End); + assert_eq!( + tui.state.searches_table_state.selected(), + Some(0), + "End is still a row key, $ is the name key" + ); + press(&mut tui, KeyCode::Char('$')); + assert!(tui.state.searches_query_offset > 8); + let screen = screen_of(&mut tui); + assert!(screen.contains("…zzzz"), "{screen}"); + assert!( + tui.state.results_name_offset == 0, + "each list scrolls on its own" + ); + } + + fn browsed(tui: &mut MainTui, files: usize) { + tui.state.browse.open("bob"); + let listing = vec![soulseek_rs::SharedDirectory { + name: "Music".to_string(), + files: (0..files).map(|i| (format!("{i:02}.mp3"), 1)).collect(), + }]; + tui.state + .browse + .active_tab_mut() + .expect("tab") + .load(&listing); + tui.state.show_browse = true; + let _ = screen_of(tui); + } + + #[test] + fn page_keys_walk_the_browse_tree_and_ctrl_d_does_not_download() { + let session = Arc::new(TalkativeSession::default()); + let mut tui = attach(session); + browsed(&mut tui, 40); + let rows = tui.state.browse.active_tab().expect("tab").rows().len(); + assert_eq!(rows, 41, "the folder and its files"); + + press(&mut tui, KeyCode::End); + let selected = |tui: &MainTui| { + tui.state.browse.active_tab().expect("tab").selected_row + }; + assert_eq!(selected(&tui), 40); + press(&mut tui, KeyCode::Home); + assert_eq!(selected(&tui), 0); + press(&mut tui, KeyCode::PageDown); + let page = selected(&tui); + assert!(page > 1 && page < 40, "a page of rows: {page}"); + ctrl(&mut tui, 'd'); + assert!(selected(&tui) > page, "ctrl-d pages"); + assert!( + tui.state.downloads.is_empty() + && tui.state.downloads_receiver_channel.is_none(), + "and does not download the folder" + ); + press(&mut tui, KeyCode::Char('G')); + assert_eq!(selected(&tui), 40); + assert!(tui.state.show_browse, "still open"); + } + + fn in_a_room(tui: &mut MainTui, messages: usize) { + tui.state.rooms.apply_event( + soulseek_rs::RoomEvent::Joined { + room: "jazz".to_string(), + users: (0..30).map(|i| format!("user{i:02}")).collect(), + }, + None, + ); + tui.state.rooms.focus_or_open("jazz"); + for i in 0..messages { + tui.state.rooms.apply_event( + soulseek_rs::RoomEvent::Message { + room: "jazz".to_string(), + username: "alice".to_string(), + message: format!("line {i:03}"), + }, + Some("jazz"), + ); + } + tui.state.show_rooms = true; + let _ = screen_of(tui); + } + + #[test] + fn page_up_scrolls_a_room_log_back_and_end_returns_to_the_newest() { + let mut tui = with_session(TalkativeSession::default()); + in_a_room(&mut tui, 120); + + let screen = screen_of(&mut tui); + assert!(screen.contains("line 119"), "tails by default: {screen}"); + assert!(!screen.contains("line 000"), "{screen}"); + + press(&mut tui, KeyCode::PageUp); + let screen = screen_of(&mut tui); + assert!(!screen.contains("line 119"), "scrolled back: {screen}"); + + press(&mut tui, KeyCode::Home); + let screen = screen_of(&mut tui); + assert!(screen.contains("line 000"), "the oldest: {screen}"); + press(&mut tui, KeyCode::PageUp); + let screen = screen_of(&mut tui); + assert!(screen.contains("line 000"), "clamped at the top: {screen}"); + + press(&mut tui, KeyCode::End); + let screen = screen_of(&mut tui); + assert!(screen.contains("line 119"), "{screen}"); + + // The member list keeps its own keys, and ctrl-x is not x. + press(&mut tui, KeyCode::Down); + assert_eq!(tui.state.rooms.user_selected, 1); + ctrl(&mut tui, 'x'); + assert_eq!(tui.state.rooms.open.len(), 1, "still in the room"); + } + + #[test] + fn page_keys_move_through_the_room_list() { + let mut tui = with_session(TalkativeSession::default()); + tui.state.rooms.apply_event( + soulseek_rs::RoomEvent::List( + (0..60) + .map(|i| soulseek_rs::types::RoomInfo { + name: format!("room{i:02}"), + user_count: 60 - i, + }) + .collect(), + ), + None, + ); + tui.state.show_rooms = true; + let _ = screen_of(&mut tui); + + press(&mut tui, KeyCode::End); + assert_eq!(tui.state.rooms.list_selected, 59); + press(&mut tui, KeyCode::PageUp); + assert!(tui.state.rooms.list_selected < 59); + press(&mut tui, KeyCode::Char('g')); + assert_eq!(tui.state.rooms.list_selected, 0); + ctrl(&mut tui, 'f'); + assert!(tui.state.rooms.list_selected > 1); + } + + #[test] + fn page_up_scrolls_a_conversation_and_switching_chats_resets_it() { + let mut tui = tui((0..80) + .map(|i| ChatMessageDto { + peer: "bob".into(), + outgoing: i % 2 == 0, + text: format!("msg {i:03}"), + at: i, + }) + .chain(std::iter::once(ChatMessageDto { + peer: "carol".into(), + outgoing: false, + text: "hello".into(), + at: 100, + })) + .collect()); + tui.state.chat_peer = Some("bob".to_string()); + tui.state.show_messages = true; + let screen = screen_of(&mut tui); + assert!(screen.contains("msg 079"), "{screen}"); + + press(&mut tui, KeyCode::PageUp); + let screen = screen_of(&mut tui); + assert!(!screen.contains("msg 079"), "scrolled back: {screen}"); + press(&mut tui, KeyCode::Char('g')); + let screen = screen_of(&mut tui); + assert!(screen.contains("msg 000"), "{screen}"); + + press(&mut tui, KeyCode::Tab); + assert_eq!(tui.state.active_chat_peer(), Some("carol")); + assert!( + tui.state.chat_view.following(), + "a fresh chat starts at its end" + ); + press(&mut tui, KeyCode::BackTab); + let screen = screen_of(&mut tui); + assert!(screen.contains("msg 079"), "{screen}"); + } + #[test] fn the_shortcut_bar_wraps_instead_of_cutting_keys_off() { let mut tui = furnished_tui(); diff --git a/soulseek-rs/src/ui/main_tui/render.rs b/soulseek-rs/src/ui/main_tui/render.rs index 06f7b9c..c7ca5b2 100644 --- a/soulseek-rs/src/ui/main_tui/render.rs +++ b/soulseek-rs/src/ui/main_tui/render.rs @@ -50,6 +50,8 @@ const HELP_LEFT: &[(&str, &[(&str, &str)])] = &[ ("Home End / g G", "first / last row"), ("PgUp PgDn / ^b ^f", "one page"), ("^u ^d", "half a page"), + ("h l / ← →", "scroll a long name sideways"), + ("0 / $", "its start / its end"), ], ), ]; @@ -71,8 +73,6 @@ const HELP_RIGHT: &[(&str, &[(&str, &str)])] = &[ ("a / A", "select all / none"), ("Enter", "download the selection"), ("/", "filter, Enter keeps it, Esc clears"), - ("h l / ← →", "scroll the file name"), - ("0 / $", "start / end of the file name"), ("b", "browse the owner"), ("c", "chat rooms"), ], @@ -87,6 +87,15 @@ const HELP_RIGHT: &[(&str, &[(&str, &str)])] = &[ ("c", "clear every finished one"), ], ), + ( + "Chat and browse popups", + &[ + ("PgUp PgDn / ^b ^f", "a page of messages, or of the tree"), + ("^u ^d", "half a page of them"), + ("Home End / g G", "oldest / newest, or first / last row"), + ("Tab / Shift-Tab", "next room, chat or user"), + ], + ), ]; impl MainTui { @@ -192,6 +201,7 @@ impl MainTui { &self.state.searches, &mut self.state.searches_table_state, focused, + self.state.searches_query_offset, ); } FocusedPane::Results => { @@ -207,6 +217,7 @@ impl MainTui { &self.state.uploads, &mut self.state.downloads_table_state, focused, + self.state.downloads_name_offset, ); } } @@ -329,6 +340,7 @@ impl MainTui { // Browse tree overlays everything when open. if self.state.show_browse && !self.state.browse.is_empty() { let area = centered_rect(80, 80, frame.area()); + self.state.popup_area = Some(area); frame.render_widget(ratatui::widgets::Clear, area); render_browse_pane( frame, @@ -347,11 +359,12 @@ impl MainTui { // Chat rooms overlay everything when open. if self.state.show_rooms { let area = centered_rect(85, 80, frame.area()); + self.state.popup_area = Some(area); frame.render_widget(ratatui::widgets::Clear, area); render_rooms_pane( frame, area, - &self.state.rooms, + &mut self.state.rooms, &mut self.state.rooms_list_table_state, ); } @@ -430,12 +443,14 @@ impl MainTui { ); } - fn render_messages_popup(&self, frame: &mut Frame) { + fn render_messages_popup(&mut self, frame: &mut Frame) { // The per-conversation chat box supersedes the old flat message list; // it renders the messages and the compose line itself. let area = centered_rect(70, 60, frame.area()); frame.render_widget(ratatui::widgets::Clear, area); - render_chat_pane(frame, area, &self.state, &self.client.username()); + self.state.popup_area = Some(area); + let own = self.client.username(); + render_chat_pane(frame, area, &mut self.state, &own); } /// Context shortcuts for the chat-rooms popup. @@ -467,6 +482,7 @@ impl MainTui { } RoomsView::Chat => vec![ ("Enter", "say"), + ("PgUp/PgDn", "scroll"), ("↑↓", "pick user"), ("b", "browse user"), ("m", "message user"), @@ -520,6 +536,7 @@ impl MainTui { } else { vec![ ("Enter", "type"), + ("PgUp/PgDn", "scroll"), ("↑↓/Tab", "switch chat"), ("m", "new chat"), ("i/Esc", "close"), @@ -530,6 +547,7 @@ impl MainTui { } else if self.state.show_browse { vec![ ("↑↓", "move"), + ("PgUp/PgDn", "page"), ("→←", "expand/collapse"), ("Enter", "open/download"), ("d", "download folder"), @@ -579,6 +597,7 @@ impl MainTui { ("i", "inbox"), ("c", "chat"), ("b", "browse user"), + ("h/l", "scroll query"), ], FocusedPane::Results => vec![ ("Space", "select"), @@ -597,6 +616,7 @@ impl MainTui { ("d", "delete queued/done"), ("c", "clear finished"), ("b", "browse user"), + ("h/l", "scroll name"), ], }; keys.extend(self.pane_shortcuts()); diff --git a/soulseek-rs/src/ui/main_tui/rooms.rs b/soulseek-rs/src/ui/main_tui/rooms.rs index bd048aa..86c033a 100644 --- a/soulseek-rs/src/ui/main_tui/rooms.rs +++ b/soulseek-rs/src/ui/main_tui/rooms.rs @@ -1,4 +1,5 @@ use super::MainTui; +use super::input::{jumped, list_jump, scroll_log}; use crate::models::RoomsView; use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; @@ -38,6 +39,16 @@ impl MainTui { fn handle_rooms_list_input(&mut self, key: KeyEvent) { let len = self.state.rooms.filtered_rooms().len(); + if let Some(jump) = list_jump(key, self.popup_page()) { + self.state.rooms.list_selected = + jumped(self.state.rooms.list_selected, len, jump); + return; + } + // Control combinations are only ever jumps; the letters below are + // not theirs to trigger. + if key.modifiers.contains(KeyModifiers::CONTROL) { + return; + } match key.code { // Esc peels back one level: clear a lingering filter first (as the // title's "Esc: clear" promises), otherwise close the popup. @@ -92,6 +103,16 @@ impl MainTui { } fn handle_rooms_chat_input(&mut self, key: KeyEvent) { + // Paging keys move through the room's history; the member list is a + // row at a time. + if let Some(view) = self.state.rooms.active_view_mut() + && scroll_log(view, key) + { + return; + } + if key.modifiers.contains(KeyModifiers::CONTROL) { + return; + } match key.code { KeyCode::Char('q') => self.state.show_rooms = false, KeyCode::Esc | KeyCode::Char('l') => { @@ -208,15 +229,23 @@ impl MainTui { self.state.rooms.composing = false; } + /// The room whose newest messages are on screen, if any: the active one + /// in the chat view, unless the reader is held back in its history, when + /// what arrives still counts as unread. + pub(super) fn viewed_room(&self) -> Option { + if !self.state.show_rooms || self.state.rooms.view != RoomsView::Chat { + return None; + } + self.state + .rooms + .active_room() + .filter(|room| room.view.following()) + .map(|room| room.name.clone()) + } + /// Drain chat-room events into the rooms state, tracking unread badges. pub(super) fn poll_room_events(&mut self) { - let viewing = if self.state.show_rooms - && self.state.rooms.view == RoomsView::Chat - { - self.state.rooms.active_room().map(|r| r.name.clone()) - } else { - None - }; + let viewing = self.viewed_room(); for event in self.client.take_room_events() { self.state.rooms.apply_event(event, viewing.as_deref()); } diff --git a/soulseek-rs/src/ui/panes/chat_pane.rs b/soulseek-rs/src/ui/panes/chat_pane.rs index 80bfc23..5a2ff83 100644 --- a/soulseek-rs/src/ui/panes/chat_pane.rs +++ b/soulseek-rs/src/ui/panes/chat_pane.rs @@ -1,4 +1,4 @@ -use crate::models::{AppState, MessageDirection}; +use crate::models::{AppState, ChatMessage, LogView, MessageDirection}; use crate::ui::{ accent_style, border_style, dimmed_style, highlight_style, info_style, primary_style, wrap_chat_line, @@ -17,10 +17,11 @@ use ratatui::{ pub fn render_chat_pane( frame: &mut Frame, area: Rect, - state: &AppState, + state: &mut AppState, own_username: &str, ) { - let peer = state.active_chat_peer(); + let peer = state.active_chat_peer().map(str::to_string); + let peer = peer.as_deref(); let title = peer.map_or_else( || " Messages (m: compose, i/Esc: close) ".to_string(), |peer| format!(" {peer} (↑↓/Tab: switch, m: to…, i/Esc: close) "), @@ -51,7 +52,14 @@ pub fn render_chat_pane( return; }; - render_messages(frame, body[0], state, peer, own_username); + render_messages( + frame, + body[0], + &state.messages, + &mut state.chat_view, + peer, + own_username, + ); render_compose(frame, chunks[1], state); } @@ -94,13 +102,13 @@ fn render_peers( fn render_messages( frame: &mut Frame, area: Rect, - state: &AppState, + messages: &[ChatMessage], + view: &mut LogView, peer: &str, own_username: &str, ) { let width = area.width as usize; - let lines: Vec = state - .messages + let lines: Vec = messages .iter() .filter(|m| m.peer == peer) .flat_map(|m| { @@ -123,9 +131,9 @@ fn render_messages( }) .collect(); - // Auto-scroll: tail by rendered rows so the newest message stays visible. - let start = lines.len().saturating_sub((area.height as usize).max(1)); - frame.render_widget(Paragraph::new(lines[start..].to_vec()), area); + // The newest rows, unless the reader is holding a place in the history. + let window = view.window(lines.len(), usize::from(area.height)); + frame.render_widget(Paragraph::new(lines[window].to_vec()), area); } fn render_compose(frame: &mut Frame, area: Rect, state: &AppState) { diff --git a/soulseek-rs/src/ui/panes/downloads_pane.rs b/soulseek-rs/src/ui/panes/downloads_pane.rs index 2684fda..e624279 100644 --- a/soulseek-rs/src/ui/panes/downloads_pane.rs +++ b/soulseek-rs/src/ui/panes/downloads_pane.rs @@ -1,3 +1,4 @@ +use super::name_scroll::{column_width, end_offset, scroll_text}; use crate::models::DownloadEntry; use crate::ui::{ GLYPH_ACTIVE, GLYPH_DONE, GLYPH_FAILED, GLYPH_QUEUED, HIGHLIGHT_SYMBOL, @@ -8,12 +9,43 @@ use crate::ui::{ }; use ratatui::{ Frame, - layout::Rect, + layout::{Constraint, Rect}, widgets::{Cell, HighlightSpacing, Paragraph, Row, Table, TableState}, }; use soulseek_rs::DownloadStatus; use soulseek_rs::types::{UploadInfo, UploadStatus}; +// Sized to the widest value each column shows in ordinary use — a +// three-digit megabyte count on both sides of the progress — so the file +// name keeps whatever width the pane has beyond that. +const WIDTHS: [Constraint; 5] = [ + Constraint::Length(6), + Constraint::Fill(1), + Constraint::Length(12), + Constraint::Length(25), + Constraint::Length(10), +]; + +/// Filename is the second column. +const NAME_COLUMN: usize = 1; + +/// The offset that brings the end of a transfer's name into view. +#[must_use] +pub fn name_end_offset(name: &str, area: Rect) -> usize { + end_offset(name, area, &WIDTHS, NAME_COLUMN) +} + +/// What the list shows for an upload: the peer-facing path is ours, so only +/// the file's own name is worth a column. +#[must_use] +pub fn upload_display_name(upload: &UploadInfo) -> &str { + upload + .filename + .rsplit(['\\', '/']) + .next() + .unwrap_or(&upload.filename) +} + pub fn render_downloads_pane( frame: &mut Frame, area: Rect, @@ -21,6 +53,7 @@ pub fn render_downloads_pane( uploads: &[UploadInfo], table_state: &mut TableState, focused: bool, + name_offset: usize, ) { if downloads.is_empty() && uploads.is_empty() { let empty_block = pane_block(focused).title(pane_title( @@ -47,6 +80,7 @@ pub fn render_downloads_pane( ]) .height(1); + let name_width = column_width(area, &WIDTHS, NAME_COLUMN); let mut rows: Vec = downloads .iter() .map(|download_entry| { @@ -106,7 +140,12 @@ pub fn render_downloads_pane( Row::new(vec![ Cell::from(status_icon).style(status_style), - Cell::from(download.filename.clone()).style(body_style()), + Cell::from(scroll_text( + &download.filename, + name_offset, + name_width, + )) + .style(body_style()), Cell::from(download.username.clone()).style(info_style()), Cell::from(progress_text).style(progress_style), Cell::from(speed_text).style(warning_style()), @@ -149,32 +188,21 @@ pub fn render_downloads_pane( } _ => "-".to_string(), }; - let basename = upload - .filename - .rsplit(['\\', '/']) - .next() - .unwrap_or(&upload.filename); Row::new(vec![ Cell::from(format!("↑ {status_icon}")).style(status_style), - Cell::from(basename.to_string()).style(body_style()), + Cell::from(scroll_text( + upload_display_name(upload), + name_offset, + name_width, + )) + .style(body_style()), Cell::from(upload.username.clone()).style(info_style()), Cell::from(progress_text).style(primary_style()), Cell::from(speed_text).style(warning_style()), ]) })); - // Sized to the widest value each column shows in ordinary use — a - // three-digit megabyte count on both sides of the progress — so the file - // name keeps whatever width the pane has beyond that. - let widths = [ - ratatui::layout::Constraint::Length(6), - ratatui::layout::Constraint::Fill(1), - ratatui::layout::Constraint::Length(12), - ratatui::layout::Constraint::Length(25), - ratatui::layout::Constraint::Length(10), - ]; - - let table = Table::new(rows, widths) + let table = Table::new(rows, WIDTHS) .header(header) .row_highlight_style(row_highlight_style()) .highlight_symbol(HIGHLIGHT_SYMBOL) diff --git a/soulseek-rs/src/ui/panes/mod.rs b/soulseek-rs/src/ui/panes/mod.rs index cc31cd9..357df0a 100644 --- a/soulseek-rs/src/ui/panes/mod.rs +++ b/soulseek-rs/src/ui/panes/mod.rs @@ -2,6 +2,7 @@ mod browse_pane; mod chat_pane; mod download_info_pane; mod downloads_pane; +mod name_scroll; mod results_pane; mod rooms_pane; mod searches_pane; @@ -11,9 +12,12 @@ pub use chat_pane::render_chat_pane; pub use download_info_pane::{ InfoSubject, render_download_info_pane, selected_transfer, }; -pub use downloads_pane::render_downloads_pane; +pub use downloads_pane::{ + name_end_offset as transfer_name_end_offset, render_downloads_pane, + upload_display_name, +}; pub use results_pane::{ ResultsPaneParams, name_end_offset, render_results_pane, }; pub use rooms_pane::render_rooms_pane; -pub use searches_pane::render_searches_pane; +pub use searches_pane::{query_end_offset, render_searches_pane}; diff --git a/soulseek-rs/src/ui/panes/name_scroll.rs b/soulseek-rs/src/ui/panes/name_scroll.rs new file mode 100644 index 0000000..99c81f1 --- /dev/null +++ b/soulseek-rs/src/ui/panes/name_scroll.rs @@ -0,0 +1,126 @@ +//! Sideways scrolling for a table column that holds text wider than it is: +//! a file name, a query. Every list scrolls the same way, so the arithmetic +//! lives here and each pane only says which of its columns it applies to. + +use crate::ui::{HIGHLIGHT_SYMBOL, pane_block}; +use ratatui::{ + layout::{Constraint, Layout, Rect}, + text::{Line, Span}, +}; +use unicode_width::UnicodeWidthChar; + +/// Cells column `column` of a table laid out with `widths` gets inside a pane +/// drawn over `area`: what the pane keeps for its border, padding and the +/// highlight symbol comes off first. +#[must_use] +pub fn column_width(area: Rect, widths: &[Constraint], column: usize) -> usize { + let inner = pane_block(false).inner(area); + let symbol = Line::from(HIGHLIGHT_SYMBOL).width() as u16; + let [_, columns] = + Layout::horizontal([Constraint::Length(symbol), Constraint::Fill(0)]) + .areas(inner); + usize::from( + Layout::horizontal(widths).spacing(1).split(columns)[column].width, + ) +} + +/// How far a text of `cells` can scroll before its tail is in view, keeping +/// one cell for the ellipsis that marks the cut. +const fn overflow(cells: usize, width: usize) -> usize { + if cells <= width { 0 } else { cells + 1 - width } +} + +/// The offset that brings the end of `text` into view in the column. +#[must_use] +pub fn end_offset( + text: &str, + area: Rect, + widths: &[Constraint], + column: usize, +) -> usize { + overflow(Span::raw(text).width(), column_width(area, widths, column)) +} + +/// `text` scrolled `offset` cells to the left, with an ellipsis where it was +/// cut. Scrolls by cells, so a wide glyph counts double, and never starts on +/// a combining mark. Beyond the end it shows the tail. +#[must_use] +pub fn scroll_text(text: &str, offset: usize, width: usize) -> Line<'_> { + if offset == 0 { + return Line::from(text); + } + let mut tail_cells = 0; + let mut furthest = text.len(); + for (index, ch) in text.char_indices().rev() { + let cells = ch.width().unwrap_or(0); + if tail_cells + cells >= width { + if index == 0 && tail_cells + cells == width { + return Line::from(text); + } + break; + } + tail_cells += cells; + furthest = index; + } + if furthest == 0 { + return Line::from(text); + } + let mut skipped = 0; + let mut start = furthest; + for (index, ch) in text.char_indices() { + if index >= furthest || skipped >= offset { + start = index; + break; + } + skipped += ch.width().unwrap_or(0); + } + while let Some(ch) = text[start..].chars().next() + && ch.width() == Some(0) + { + start += ch.len_utf8(); + } + Line::from(vec![Span::raw("…"), Span::raw(&text[start..])]) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn text_of(line: &Line<'_>) -> String { + line.spans.iter().map(|s| s.content.as_ref()).collect() + } + + #[test] + fn a_text_that_fits_never_scrolls() { + assert_eq!(text_of(&scroll_text("short", 8, 10)), "short"); + assert_eq!( + end_offset( + "short", + Rect::new(0, 0, 40, 5), + &[Constraint::Fill(1)], + 0 + ), + 0 + ); + } + + #[test] + fn the_end_offset_leaves_room_for_the_ellipsis() { + // Column of 40 - 2 border - 2 padding - 1 symbol = 35 cells. + let area = Rect::new(0, 0, 40, 5); + let widths = [Constraint::Fill(1)]; + let fits = "x".repeat(35); + let over = "x".repeat(36); + assert_eq!(end_offset(&fits, area, &widths, 0), 0); + assert_eq!(end_offset(&over, area, &widths, 0), 2); + let line = scroll_text(&over, 2, 35); + assert_eq!(text_of(&line).chars().count(), 35); + assert!(text_of(&line).starts_with('…')); + } + + #[test] + fn scrolling_past_the_end_shows_the_tail() { + let line = scroll_text("abcdefghij", usize::MAX, 5); + assert_eq!(text_of(&line), "…ghij"); + } +} diff --git a/soulseek-rs/src/ui/panes/results_pane.rs b/soulseek-rs/src/ui/panes/results_pane.rs index 1634047..039c7bf 100644 --- a/soulseek-rs/src/ui/panes/results_pane.rs +++ b/soulseek-rs/src/ui/panes/results_pane.rs @@ -1,3 +1,4 @@ +use super::name_scroll::{column_width, end_offset, scroll_text}; use crate::models::FileDisplayData; use crate::ui::{ BYTES_PER_MB, HIGHLIGHT_SYMBOL, body_style, dimmed_style, format_bytes, @@ -6,12 +7,11 @@ use crate::ui::{ }; use ratatui::{ Frame, - layout::{Constraint, Layout, Rect}, + layout::{Constraint, Rect}, text::{Line, Span}, widgets::{Cell, HighlightSpacing, Paragraph, Row, Table, TableState}, }; use std::collections::HashSet; -use unicode_width::UnicodeWidthChar; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -39,59 +39,12 @@ pub struct ResultsPaneParams<'a> { pub name_offset: usize, } -fn name_column_width(area: Rect) -> usize { - let inner = pane_block(false).inner(area); - let symbol = Line::from(HIGHLIGHT_SYMBOL).width() as u16; - let [_, columns] = - Layout::horizontal([Constraint::Length(symbol), Constraint::Fill(0)]) - .areas(inner); - usize::from(Layout::horizontal(WIDTHS).spacing(1).split(columns)[1].width) -} - -const fn overflow(cells: usize, width: usize) -> usize { - if cells <= width { 0 } else { cells + 1 - width } -} +/// Filename is the second column. +const NAME_COLUMN: usize = 1; #[must_use] pub fn name_end_offset(name: &str, area: Rect) -> usize { - overflow(Span::raw(name).width(), name_column_width(area)) -} - -fn scroll_name(name: &str, offset: usize, width: usize) -> Line<'_> { - if offset == 0 { - return Line::from(name); - } - let mut tail_cells = 0; - let mut furthest = name.len(); - for (index, ch) in name.char_indices().rev() { - let cells = ch.width().unwrap_or(0); - if tail_cells + cells >= width { - if index == 0 && tail_cells + cells == width { - return Line::from(name); - } - break; - } - tail_cells += cells; - furthest = index; - } - if furthest == 0 { - return Line::from(name); - } - let mut skipped = 0; - let mut start = furthest; - for (index, ch) in name.char_indices() { - if index >= furthest || skipped >= offset { - start = index; - break; - } - skipped += ch.width().unwrap_or(0); - } - while let Some(ch) = name[start..].chars().next() - && ch.width() == Some(0) - { - start += ch.len_utf8(); - } - Line::from(vec![Span::raw("…"), Span::raw(&name[start..])]) + end_offset(name, area, &WIDTHS, NAME_COLUMN) } /// Whether the rendered row `display_idx` is selected. `selected_indices` holds @@ -173,7 +126,7 @@ pub fn render_results_pane( ]) .height(1); - let name_width = name_column_width(area); + let name_width = column_width(area, &WIDTHS, NAME_COLUMN); let rows: Vec = items .iter() .enumerate() @@ -206,7 +159,7 @@ pub fn render_results_pane( Row::new(vec![ Cell::from(checkbox).style(checkbox_style), - Cell::from(scroll_name( + Cell::from(scroll_text( &file.filename, name_offset, name_width, diff --git a/soulseek-rs/src/ui/panes/rooms_pane.rs b/soulseek-rs/src/ui/panes/rooms_pane.rs index 53cd407..18624dd 100644 --- a/soulseek-rs/src/ui/panes/rooms_pane.rs +++ b/soulseek-rs/src/ui/panes/rooms_pane.rs @@ -1,4 +1,4 @@ -use crate::models::{RoomsState, RoomsView}; +use crate::models::{LogView, RoomsState, RoomsView}; use crate::ui::{ HIGHLIGHT_SYMBOL, PANE_PADDING, accent_style, dimmed_style, highlight_style, info_style, pane_block, plain_title, primary_style, @@ -20,7 +20,7 @@ use ratatui::{ pub fn render_rooms_pane( frame: &mut Frame, area: Rect, - rooms: &RoomsState, + rooms: &mut RoomsState, list_table_state: &mut TableState, ) { match rooms.view { @@ -94,7 +94,7 @@ fn render_list( frame.render_stateful_widget(table, area, table_state); } -fn render_chat(frame: &mut Frame, area: Rect, rooms: &RoomsState) { +fn render_chat(frame: &mut Frame, area: Rect, rooms: &mut RoomsState) { let block = pane_block(true).title( " Chat rooms (Tab: switch, l: room list, x: leave, Esc: back) ", ); @@ -110,7 +110,10 @@ fn render_chat(frame: &mut Frame, area: Rect, rooms: &RoomsState) { render_tab_bar(frame, chunks[0], rooms); - let Some(active) = rooms.active_room() else { + let user_selected = rooms.user_selected; + let composing = rooms.composing; + let active_index = rooms.active; + let Some(active) = rooms.open.get_mut(active_index) else { frame.render_widget( Paragraph::new("No open rooms. Press l for the room list.") .style(dimmed_style()), @@ -124,11 +127,11 @@ fn render_chat(frame: &mut Frame, area: Rect, rooms: &RoomsState) { Layout::horizontal([Constraint::Fill(1), Constraint::Length(22)]) .split(chunks[1]); - render_messages(frame, body[0], active.lines.as_slice()); - render_users(frame, body[1], &active.users, rooms.user_selected); + render_messages(frame, body[0], active.lines.as_slice(), &mut active.view); + render_users(frame, body[1], &active.users, user_selected); // Compose line or hint. - if rooms.composing { + if composing { let line = Line::from(vec![ Span::styled("› ", accent_style()), Span::styled(active.input.clone(), primary_style()), @@ -173,9 +176,10 @@ fn render_messages( frame: &mut Frame, area: Rect, lines: &[crate::models::RoomLine], + view: &mut LogView, ) { - // Wrap each message to the pane width, then auto-scroll: tail by rendered - // rows so the newest message is always fully visible. + // Wrap each message to the pane width, then show the rows the reader is + // at: the newest ones, unless they are holding a place in the history. let width = area.width as usize; let rendered: Vec = lines .iter() @@ -198,8 +202,8 @@ fn render_messages( } }) .collect(); - let start = rendered.len().saturating_sub((area.height as usize).max(1)); - frame.render_widget(Paragraph::new(rendered[start..].to_vec()), area); + let window = view.window(rendered.len(), usize::from(area.height)); + frame.render_widget(Paragraph::new(rendered[window].to_vec()), area); } fn render_users( diff --git a/soulseek-rs/src/ui/panes/searches_pane.rs b/soulseek-rs/src/ui/panes/searches_pane.rs index 8942d83..03d45eb 100644 --- a/soulseek-rs/src/ui/panes/searches_pane.rs +++ b/soulseek-rs/src/ui/panes/searches_pane.rs @@ -1,3 +1,4 @@ +use super::name_scroll::{column_width, end_offset, scroll_text}; use crate::models::{SearchEntry, SearchStatus}; use crate::ui::{ HIGHLIGHT_SYMBOL, body_style, dimmed_style, header_style, pane_block, @@ -5,16 +6,34 @@ use crate::ui::{ }; use ratatui::{ Frame, - layout::Rect, + layout::{Constraint, Rect}, widgets::{Cell, HighlightSpacing, Row, Table, TableState}, }; +// The pane shares a row with two others, so the fixed columns are as +// narrow as their widest word: the query gets the rest. +const WIDTHS: [Constraint; 3] = [ + Constraint::Length(6), + Constraint::Fill(1), + Constraint::Length(7), +]; + +/// Query is the second column. +const QUERY_COLUMN: usize = 1; + +/// The offset that brings the end of a query into view. +#[must_use] +pub fn query_end_offset(query: &str, area: Rect) -> usize { + end_offset(query, area, &WIDTHS, QUERY_COLUMN) +} + pub fn render_searches_pane( frame: &mut Frame, area: Rect, searches: &[SearchEntry], table_state: &mut TableState, focused: bool, + query_offset: usize, ) { let header = Row::new(vec![ Cell::from("Status").style(header_style()), @@ -23,6 +42,7 @@ pub fn render_searches_pane( ]) .height(1); + let query_width = column_width(area, &WIDTHS, QUERY_COLUMN); let rows: Vec = searches .iter() .map(|search| { @@ -55,21 +75,18 @@ pub fn render_searches_pane( Row::new(vec![ status_cell, - Cell::from(search.query.clone()).style(body_style()), + Cell::from(scroll_text( + &search.query, + query_offset, + query_width, + )) + .style(body_style()), Cell::from(results_text).style(dimmed_style()), ]) }) .collect(); - // The pane shares a row with two others, so the fixed columns are as - // narrow as their widest word: the query gets the rest. - let widths = [ - ratatui::layout::Constraint::Length(6), - ratatui::layout::Constraint::Fill(1), - ratatui::layout::Constraint::Length(7), - ]; - - let table = Table::new(rows, widths) + let table = Table::new(rows, WIDTHS) .header(header) .row_highlight_style(row_highlight_style()) .highlight_symbol(HIGHLIGHT_SYMBOL) diff --git a/website/src/pages/docs.tsx b/website/src/pages/docs.tsx index 0142507..90691ed 100644 --- a/website/src/pages/docs.tsx +++ b/website/src/pages/docs.tsx @@ -36,7 +36,8 @@ const PANE_KEYS: readonly (readonly [string, string])[] = [ ['w', 'hide the focused pane; the others widen'], ['↑ ↓ · j k', 'move a row in any list'], ['Home End · g G', 'first / last row'], - ['PgUp PgDn · ^u ^d', 'a page, or half a page'], + ['PgUp PgDn · ^u ^d', 'a page, or half a page; in a chat, back through the log'], + ['h l · ← →', 'scroll a long name or query sideways, 0 and $ to either end'], ] const ROOM_KEYS: readonly (readonly [string, string])[] = [ From d4e86187d28d0daffe82759ac4ed94390bc0fc4f Mon Sep 17 00:00:00 2001 From: Michel de Graaf Date: Mon, 7 Sep 2026 10:51:41 +0200 Subject: [PATCH 3/3] refactor(tui): fold the keys overlay onto LogView and dedupe the pane lookups The keys overlay scrolled with its own counter, a fixed page of ten and a usize::MAX sentinel that the renderer clamped back; it now reads through the same LogView as the chat and room logs, so the page is the window it last drew and the half-page keys work there too. One pane_area lookup and one page_of helper replace the pane-to-area match that page_size, highlighted_name_end and the mouse hit test each repeated, and list_jump takes its page as a usize so the callers stop converting. The pane number keys are one range arm over FocusedPane::ALL, whose index is the declaration order. The row layout carries each pane's weight beside it instead of re-matching with an unreachable arm, and the help columns are measured from the lines already built rather than built again for their width. The shortcut legend borrows its static keys instead of allocating three strings per entry per frame. --- soulseek-rs/src/models/app_state.rs | 29 +++-- soulseek-rs/src/ui/main_tui/input.rs | 153 ++++++++++-------------- soulseek-rs/src/ui/main_tui/render.rs | 63 ++++------ soulseek-rs/src/ui/panes/name_scroll.rs | 6 +- soulseek-rs/src/ui/panes/rooms_pane.rs | 16 ++- soulseek-rs/src/ui/styles.rs | 37 +++--- 6 files changed, 138 insertions(+), 166 deletions(-) diff --git a/soulseek-rs/src/models/app_state.rs b/soulseek-rs/src/models/app_state.rs index 0030668..3c8a786 100644 --- a/soulseek-rs/src/models/app_state.rs +++ b/soulseek-rs/src/models/app_state.rs @@ -41,15 +41,12 @@ pub enum FocusedPane { impl FocusedPane { /// Every focusable pane, in the order `Tab` walks them: the number each - /// carries in its legend. + /// carries in its legend, which is also declaration order. pub const ALL: [Self; 3] = [Self::Searches, Self::Results, Self::Downloads]; + /// Its position in [`Self::ALL`]. const fn index(self) -> usize { - match self { - Self::Searches => 0, - Self::Results => 1, - Self::Downloads => 2, - } + self as usize } } @@ -58,7 +55,7 @@ impl FocusedPane { /// panes while zoomed reads as switching windows. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct PaneLayout { - hidden: [bool; 3], + hidden: [bool; FocusedPane::ALL.len()], pub zoomed: bool, } @@ -238,9 +235,9 @@ pub struct AppState { pub layout: PaneLayout, /// The keys overlay (`?`) is open. pub show_help: bool, - /// Rows the keys overlay is scrolled down, for a terminal too short to - /// show it whole. The renderer clamps it to what there is to scroll. - pub help_scroll: usize, + /// Where the keys overlay is being read, for a terminal too short to + /// show it whole. + pub help_view: LogView, pub should_exit: bool, pub command_bar_active: bool, pub command_bar_input: String, @@ -322,7 +319,7 @@ impl AppState { focused_pane: FocusedPane::Searches, layout: PaneLayout::default(), show_help: false, - help_scroll: 0, + help_view: LogView::default(), should_exit: false, command_bar_active: false, command_bar_input: String::new(), @@ -365,6 +362,16 @@ impl AppState { } } + /// Where `pane` was last drawn; `None` while it is hidden. + #[must_use] + pub const fn pane_area(&self, pane: FocusedPane) -> Option { + match pane { + FocusedPane::Searches => self.searches_pane_area, + FocusedPane::Results => self.results_pane_area, + FocusedPane::Downloads => self.downloads_pane_area, + } + } + /// Put the focus on `pane`, bringing it back on screen if it was hidden: /// pressing a pane's number is also how it comes back. pub const fn focus_pane(&mut self, pane: FocusedPane) { diff --git a/soulseek-rs/src/ui/main_tui/input.rs b/soulseek-rs/src/ui/main_tui/input.rs index 2baa211..2b7038b 100644 --- a/soulseek-rs/src/ui/main_tui/input.rs +++ b/soulseek-rs/src/ui/main_tui/input.rs @@ -8,12 +8,10 @@ use crate::ui::panes::{ use ratatui::crossterm::event::{ KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind, }; -use ratatui::layout::Position; +use ratatui::layout::{Position, Rect}; use ratatui::widgets::TableState; const NAME_SCROLL_STEP: isize = 8; -/// Rows a page key moves the keys overlay, which has no table to measure. -const HELP_PAGE: usize = 10; impl MainTui { pub(super) fn handle_key_event(&mut self, key: KeyEvent) { @@ -23,22 +21,18 @@ impl MainTui { } // The keys list closes on the key that opened it, or the usual two, - // and scrolls like any list for a terminal too short to show it all. + // and scrolls like any log for a terminal too short to show it all. if self.state.show_help { - let scroll = &mut self.state.help_scroll; + let view = &mut self.state.help_view; match key.code { KeyCode::Char('?' | 'q') | KeyCode::Esc => { self.state.show_help = false; } - KeyCode::Down | KeyCode::Char('j') => *scroll += 1, - KeyCode::Up | KeyCode::Char('k') => { - *scroll = scroll.saturating_sub(1); + KeyCode::Down | KeyCode::Char('j') => view.scroll(1), + KeyCode::Up | KeyCode::Char('k') => view.scroll(-1), + _ => { + scroll_log(view, key); } - KeyCode::PageDown => *scroll += HELP_PAGE, - KeyCode::PageUp => *scroll = scroll.saturating_sub(HELP_PAGE), - KeyCode::Home | KeyCode::Char('g') => *scroll = 0, - KeyCode::End | KeyCode::Char('G') => *scroll = usize::MAX, - _ => {} } return; } @@ -81,19 +75,13 @@ impl MainTui { } KeyCode::Char('?') => { self.state.show_help = true; - self.state.help_scroll = 0; + self.state.help_view.to_oldest(); return; } - KeyCode::Char('1') => { - self.state.focus_pane(FocusedPane::Searches); - return; - } - KeyCode::Char('2') => { - self.state.focus_pane(FocusedPane::Results); - return; - } - KeyCode::Char('3') => { - self.state.focus_pane(FocusedPane::Downloads); + // A pane's number is its place in the legend order. + KeyCode::Char(digit @ '1'..='3') => { + let index = usize::from(digit as u8 - b'1'); + self.state.focus_pane(FocusedPane::ALL[index]); return; } KeyCode::Tab => { @@ -368,7 +356,7 @@ impl MainTui { /// The list the focus is on and how far one page moves it. fn navigate_focused_list(&mut self, key: KeyEvent) -> bool { let focused = self.state.focused_pane; - let page = self.page_size(focused); + let page = self.page_size(); let (table, len) = match focused { FocusedPane::Searches => ( &mut self.state.searches_table_state, @@ -388,24 +376,14 @@ impl MainTui { /// Rows a popup shows at once: its inner height less a tab bar and a /// compose line, which is what its list or log has left. - pub(super) fn popup_page(&self) -> isize { - let rows = self.state.popup_area.map_or(1, |area| { - pane_block(false).inner(area).height.saturating_sub(2) - }); - isize::try_from(rows.max(1)).unwrap_or(isize::MAX) + pub(super) fn popup_page(&self) -> usize { + page_of(self.state.popup_area, 2) } - /// Rows a pane shows at once: its inner height less the table header. - fn page_size(&self, pane: FocusedPane) -> isize { - let area = match pane { - FocusedPane::Searches => self.state.searches_pane_area, - FocusedPane::Results => self.state.results_pane_area, - FocusedPane::Downloads => self.state.downloads_pane_area, - }; - let rows = area.map_or(1, |area| { - pane_block(false).inner(area).height.saturating_sub(1) - }); - isize::try_from(rows.max(1)).unwrap_or(isize::MAX) + /// Rows the focused pane shows at once: its inner height less the table + /// header. + fn page_size(&self) -> usize { + page_of(self.state.pane_area(self.state.focused_pane), 1) } fn handle_searches_input(&mut self, key: KeyEvent) { @@ -518,28 +496,21 @@ impl MainTui { /// How far the highlighted row's long column can scroll before its end /// is in view. Zero when nothing is highlighted or the pane is not drawn. fn highlighted_name_end(&self) -> usize { - match self.state.focused_pane { - FocusedPane::Searches => { - let Some(area) = self.state.searches_pane_area else { - return 0; - }; - self.state - .searches_table_state - .selected() - .and_then(|row| self.state.searches.get(row)) - .map_or(0, |search| query_end_offset(&search.query, area)) - } - FocusedPane::Results => { - let Some(area) = self.state.results_pane_area else { - return 0; - }; - self.highlighted_result() - .map_or(0, |file| name_end_offset(&file.filename, area)) - } + let pane = self.state.focused_pane; + let Some(area) = self.state.pane_area(pane) else { + return 0; + }; + match pane { + FocusedPane::Searches => self + .state + .searches_table_state + .selected() + .and_then(|row| self.state.searches.get(row)) + .map_or(0, |search| query_end_offset(&search.query, area)), + FocusedPane::Results => self + .highlighted_result() + .map_or(0, |file| name_end_offset(&file.filename, area)), FocusedPane::Downloads => { - let Some(area) = self.state.downloads_pane_area else { - return 0; - }; let shown = match selected_transfer( self.state.downloads_table_state.selected(), &self.state.downloads, @@ -589,24 +560,31 @@ impl MainTui { } let clicked = Position::new(mouse.column, mouse.row); - let panes = [ - (self.state.searches_pane_area, FocusedPane::Searches), - (self.state.results_pane_area, FocusedPane::Results), - (self.state.downloads_pane_area, FocusedPane::Downloads), - ]; // The areas are from the last draw. A pane hidden since then, in the // same batch of events, still has one, and is not there to click. - for (area, pane) in panes { - if area.is_some_and(|area| area.contains(clicked)) - && self.state.layout.is_visible(pane) - { - self.state.focused_pane = pane; - return; - } + let hit = FocusedPane::ALL.into_iter().find(|pane| { + self.state.layout.is_visible(*pane) + && self + .state + .pane_area(*pane) + .is_some_and(|area| area.contains(clicked)) + }); + if let Some(pane) = hit { + self.state.focused_pane = pane; } } } +/// Rows a list in `area` shows at once, less the `chrome` rows above it: a +/// table header, a popup's tab bar and compose line. At least one, so a page +/// key always moves, even before the first draw. +fn page_of(area: Option, chrome: u16) -> usize { + area.map_or(0, |area| { + usize::from(pane_block(false).inner(area).height.saturating_sub(chrome)) + }) + .max(1) +} + /// A move over a whole list at once: to either end, or by a number of rows /// (negative is up). #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -618,8 +596,9 @@ pub(super) enum ListJump { /// The keys every list and log answers alike for moving further than a row: /// the two ends, a page, half a page. `page` is how many rows the list shows. -pub(super) fn list_jump(key: KeyEvent, page: isize) -> Option { +pub(super) fn list_jump(key: KeyEvent, page: usize) -> Option { let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); + let page = isize::try_from(page).unwrap_or(isize::MAX); let half = (page / 2).max(1); Some(match key.code { KeyCode::Home | KeyCode::Char('g') if !ctrl => ListJump::First, @@ -637,8 +616,7 @@ pub(super) fn list_jump(key: KeyEvent, page: isize) -> Option { /// Apply the jump keys to a log, moving by the screenful it last showed. /// Says whether `key` was one of them. pub(super) fn scroll_log(view: &mut LogView, key: KeyEvent) -> bool { - let page = isize::try_from(view.page()).unwrap_or(isize::MAX); - match list_jump(key, page) { + match list_jump(key, view.page()) { Some(ListJump::First) => view.to_oldest(), Some(ListJump::Last) => view.to_newest(), Some(ListJump::Rows(rows)) => view.scroll(rows), @@ -663,19 +641,21 @@ fn navigate_list( key: KeyEvent, table: &mut TableState, len: usize, - page: isize, + page: usize, ) -> bool { let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); match key.code { KeyCode::Up | KeyCode::Char('k') if !ctrl => cycle(table, len, false), KeyCode::Down | KeyCode::Char('j') if !ctrl => cycle(table, len, true), - _ => match list_jump(key, page) { - Some(jump) => { + _ => { + let Some(jump) = list_jump(key, page) else { + return false; + }; + if len > 0 { let current = table.selected().unwrap_or(0); - select_row(table, len, jumped(current, len, jump)); + table.select(Some(jumped(current, len, jump))); } - None => return false, - }, + } } true } @@ -693,10 +673,3 @@ fn cycle(table: &mut TableState, len: usize, forward: bool) { }; table.select(Some(next)); } - -/// Put the selection on `target`, or the last row when that is past the end. -fn select_row(table: &mut TableState, len: usize, target: usize) { - if len > 0 { - table.select(Some(target.min(len - 1))); - } -} diff --git a/soulseek-rs/src/ui/main_tui/render.rs b/soulseek-rs/src/ui/main_tui/render.rs index c7ca5b2..166283c 100644 --- a/soulseek-rs/src/ui/main_tui/render.rs +++ b/soulseek-rs/src/ui/main_tui/render.rs @@ -6,13 +6,14 @@ use crate::ui::panes::{ render_rooms_pane, render_searches_pane, selected_transfer, }; use crate::ui::{ - SIGNAL, accent_style, body_style, dimmed_style, pack_shortcuts, pane_block, - plain_title, primary_style, render_download_stats, warning_style, + accent_style, body_style, dimmed_style, info_style, pack_shortcuts, + pane_block, plain_title, primary_style, render_download_stats, + warning_style, }; use ratatui::{ Frame, layout::{Alignment, Constraint, Layout, Position, Rect}, - style::{Modifier, Style}, + style::Modifier, text::{Line, Span}, widgets::Paragraph, }; @@ -147,10 +148,11 @@ impl MainTui { return; } - let row_panes: Vec = - [FocusedPane::Searches, FocusedPane::Downloads] + // Transfers carry the most columns, so they get the most width. + let row_panes: Vec<(FocusedPane, u16)> = + [(FocusedPane::Searches, 5), (FocusedPane::Downloads, 10)] .into_iter() - .filter(|pane| self.state.layout.is_visible(*pane)) + .filter(|(pane, _)| self.state.layout.is_visible(*pane)) .collect(); let row_area = if self.state.layout.is_visible(FocusedPane::Results) { @@ -168,18 +170,13 @@ impl MainTui { area }; - // Transfers carry the most columns, so they get the most width. let mut constraints: Vec = row_panes .iter() - .map(|pane| match pane { - FocusedPane::Searches => Constraint::Fill(5), - FocusedPane::Downloads => Constraint::Fill(10), - FocusedPane::Results => unreachable!("not a row pane"), - }) + .map(|(_, weight)| Constraint::Fill(*weight)) .collect(); constraints.push(Constraint::Fill(6)); // Info let chunks = Layout::horizontal(constraints).split(row_area); - for (chunk, pane) in chunks.iter().zip(&row_panes) { + for (chunk, (pane, _)) in chunks.iter().zip(&row_panes) { self.render_pane(frame, *chunk, *pane); } self.render_info_pane(frame, chunks[row_panes.len()]); @@ -295,8 +292,10 @@ impl MainTui { let screen = frame.area(); // Borders and padding on either side of the text. let frame_width = 4; - let width = screen.width.min(help_width() + frame_width); - let columns = help_columns(width.saturating_sub(frame_width)); + let columns = help_columns(screen.width.saturating_sub(frame_width)); + let gaps = HELP_GAP * (columns.len() as u16 - 1); + let text_width = columns.iter().map(|lines| widest(lines)).sum::(); + let width = screen.width.min(text_width + gaps + frame_width); let tallest = columns.iter().map(Vec::len).max().unwrap_or(0); let height = screen .height @@ -314,10 +313,12 @@ impl MainTui { frame.render_widget(block, area); // Both columns scroll together, and no further than the taller one - // needs; the clamp is written back so the keys cannot run past it. - let furthest = tallest.saturating_sub(usize::from(inner.height)); - self.state.help_scroll = self.state.help_scroll.min(furthest); - let scroll = u16::try_from(self.state.help_scroll).unwrap_or(u16::MAX); + // needs. + let window = self + .state + .help_view + .window(tallest, usize::from(inner.height)); + let scroll = u16::try_from(window.start).unwrap_or(u16::MAX); let areas = Layout::horizontal(vec![Constraint::Fill(1); columns.len()]) @@ -734,19 +735,10 @@ fn key_column_width(sections: &[(&str, &[(&str, &str)])]) -> usize { .unwrap_or(0) } -/// Cells a column of the keys list needs to show every line whole. -fn help_column_width(sections: &[(&str, &[(&str, &str)])]) -> u16 { - let widest = help_lines(sections) - .iter() - .map(Line::width) - .max() - .unwrap_or(0); - u16::try_from(widest).unwrap_or(u16::MAX) -} - -/// The text width of the keys list with both halves side by side. -fn help_width() -> u16 { - help_column_width(HELP_LEFT) + HELP_GAP + help_column_width(HELP_RIGHT) +/// Cells the widest of `lines` takes. +fn widest(lines: &[Line<'_>]) -> u16 { + let cells = lines.iter().map(Line::width).max().unwrap_or(0); + u16::try_from(cells).unwrap_or(u16::MAX) } /// The keys list laid out for `width` cells of text: two columns when both @@ -754,7 +746,7 @@ fn help_width() -> u16 { fn help_columns(width: u16) -> Vec>> { let left = help_lines(HELP_LEFT); let right = help_lines(HELP_RIGHT); - if width >= help_width() { + if width >= widest(&left) + HELP_GAP + widest(&right) { return vec![left, right]; } let mut stacked = left; @@ -776,10 +768,7 @@ fn help_lines(sections: &[(&str, &[(&str, &str)])]) -> Vec> { )); for (key, action) in *keys { lines.push(Line::from(vec![ - Span::styled( - format!("{key: usize { let inner = pane_block(false).inner(area); - let symbol = Line::from(HIGHLIGHT_SYMBOL).width() as u16; + let symbol = HIGHLIGHT_SYMBOL.width() as u16; let [_, columns] = Layout::horizontal([Constraint::Length(symbol), Constraint::Fill(0)]) .areas(inner); @@ -38,7 +38,7 @@ pub fn end_offset( widths: &[Constraint], column: usize, ) -> usize { - overflow(Span::raw(text).width(), column_width(area, widths, column)) + overflow(text.width(), column_width(area, widths, column)) } /// `text` scrolled `offset` cells to the left, with an ellipsis where it was diff --git a/soulseek-rs/src/ui/panes/rooms_pane.rs b/soulseek-rs/src/ui/panes/rooms_pane.rs index 18624dd..28a938f 100644 --- a/soulseek-rs/src/ui/panes/rooms_pane.rs +++ b/soulseek-rs/src/ui/panes/rooms_pane.rs @@ -110,10 +110,14 @@ fn render_chat(frame: &mut Frame, area: Rect, rooms: &mut RoomsState) { render_tab_bar(frame, chunks[0], rooms); - let user_selected = rooms.user_selected; - let composing = rooms.composing; - let active_index = rooms.active; - let Some(active) = rooms.open.get_mut(active_index) else { + let RoomsState { + open, + active: active_index, + user_selected, + composing, + .. + } = rooms; + let Some(active) = open.get_mut(*active_index) else { frame.render_widget( Paragraph::new("No open rooms. Press l for the room list.") .style(dimmed_style()), @@ -128,10 +132,10 @@ fn render_chat(frame: &mut Frame, area: Rect, rooms: &mut RoomsState) { .split(chunks[1]); render_messages(frame, body[0], active.lines.as_slice(), &mut active.view); - render_users(frame, body[1], &active.users, user_selected); + render_users(frame, body[1], &active.users, *user_selected); // Compose line or hint. - if composing { + if *composing { let line = Line::from(vec![ Span::styled("› ", accent_style()), Span::styled(active.input.clone(), primary_style()), diff --git a/soulseek-rs/src/ui/styles.rs b/soulseek-rs/src/ui/styles.rs index 930cc02..0acbaac 100644 --- a/soulseek-rs/src/ui/styles.rs +++ b/soulseek-rs/src/ui/styles.rs @@ -39,7 +39,8 @@ pub const GLYPH_FAILED: &str = "✗"; pub const GLYPH_TIMED_OUT: &str = "⧗"; pub const GLYPH_CURSOR: &str = "▮"; pub const HIGHLIGHT_SYMBOL: &str = "›"; -pub const SHORTCUT_ARROW: &str = "→"; +/// Between a key and its action in the legend, spaced. +pub const SHORTCUT_ARROW: &str = " → "; pub fn header_style() -> Style { Style::default().fg(DUST).add_modifier(Modifier::BOLD) @@ -159,12 +160,15 @@ pub fn download_status_glyph(status: &DownloadStatus) -> (&'static str, Style) { } /// One `[key → action]` legend entry. -fn shortcut_spans(key: &str, action: &str) -> Vec> { - vec![ +fn shortcut_spans( + key: &'static str, + action: &'static str, +) -> [Span<'static>; 5] { + [ Span::styled("[", dimmed_style()), - Span::styled(key.to_string(), Style::default().fg(SIGNAL)), - Span::styled(format!(" {SHORTCUT_ARROW} "), dimmed_style()), - Span::styled(action.to_string(), body_style()), + Span::styled(key, info_style()), + Span::styled(SHORTCUT_ARROW, dimmed_style()), + Span::styled(action, body_style()), Span::styled("]", dimmed_style()), ] } @@ -178,7 +182,7 @@ pub const SHORTCUT_ROWS_MAX: usize = 3; /// that do not fit in [`SHORTCUT_ROWS_MAX`] rows are dropped, so a bar always /// has at least one row and never more than that. pub fn pack_shortcuts( - shortcuts: &[(&str, &str)], + shortcuts: &[(&'static str, &'static str)], width: u16, ) -> Vec> { let width = usize::from(width); @@ -189,24 +193,19 @@ pub fn pack_shortcuts( for (key, action) in shortcuts { let entry = shortcut_spans(key, action); let entry_width: usize = entry.iter().map(Span::width).sum(); - let needed = if row.is_empty() { - entry_width - } else { - row_width + 1 + entry_width - }; - if !row.is_empty() && needed > width { + if !row.is_empty() && row_width + 1 + entry_width > width { rows.push(Line::from(std::mem::take(&mut row))); if rows.len() == SHORTCUT_ROWS_MAX { return rows; } - row_width = entry_width; - } else { - if !row.is_empty() { - row.push(Span::raw(" ")); - } - row_width = needed; + row_width = 0; + } + if !row.is_empty() { + row.push(Span::raw(" ")); + row_width += 1; } row.extend(entry); + row_width += entry_width; } rows.push(Line::from(row));