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])[] = [