From 8fff93464a12448c51a313c1e74eda92f14c3456 Mon Sep 17 00:00:00 2001
From: strNophix <nvdpoel01@gmail.com>
Date: Sun, 20 Mar 2022 19:50:55 +0100
Subject: [PATCH] Added basic music playback + tui

---
 .gitignore               |   2 +
 README.md                |   2 +-
 go.mod                   |  32 ++++++++++
 go.sum                   | 127 +++++++++++++++++++++++++++++++++++++++
 main.go                  |  15 +++++
 pkg/commands.go          |  13 ++++
 pkg/list_delegate.go     |  32 ++++++++++
 pkg/model.go             |  77 ++++++++++++++++++++++++
 pkg/status_bar.go        |  54 +++++++++++++++++
 pkg/track_fetcher.go     |  72 ++++++++++++++++++++++
 pkg/track_player.go      |  32 ++++++++++
 pkg/track_player_view.go |  53 ++++++++++++++++
 pkg/utils.go             |  17 ++++++
 13 files changed, 527 insertions(+), 1 deletion(-)
 create mode 100644 go.mod
 create mode 100644 go.sum
 create mode 100644 main.go
 create mode 100644 pkg/commands.go
 create mode 100644 pkg/list_delegate.go
 create mode 100644 pkg/model.go
 create mode 100644 pkg/status_bar.go
 create mode 100644 pkg/track_fetcher.go
 create mode 100644 pkg/track_player.go
 create mode 100644 pkg/track_player_view.go
 create mode 100644 pkg/utils.go

diff --git a/.gitignore b/.gitignore
index f4d432a..51b57ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,5 @@
 # Dependency directories (remove the comment below to include it)
 # vendor/
 
+# Test data
+samples/
diff --git a/README.md b/README.md
index 117ff7f..c329fdd 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,3 @@
 # gomus
 
-yay cmus
\ No newline at end of file
+cmus but in Go
\ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..8b0e765
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,32 @@
+module git.cesium.pw/niku/gomus
+
+go 1.18
+
+require (
+	github.com/atotto/clipboard v0.1.4 // indirect
+	github.com/bxcodec/faker/v3 v3.8.0 // indirect
+	github.com/charmbracelet/bubbles v0.10.3 // indirect
+	github.com/charmbracelet/bubbletea v0.20.0 // indirect
+	github.com/charmbracelet/lipgloss v0.5.0 // indirect
+	github.com/containerd/console v1.0.3 // indirect
+	github.com/faiface/beep v1.1.0 // indirect
+	github.com/hajimehoshi/oto v1.0.1 // indirect
+	github.com/icza/bitio v1.1.0 // indirect
+	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+	github.com/mattn/go-isatty v0.0.14 // indirect
+	github.com/mattn/go-runewidth v0.0.13 // indirect
+	github.com/mewkiz/flac v1.0.7 // indirect
+	github.com/mewkiz/pkg v0.0.0-20211102230744-16a6ce8f1b77 // indirect
+	github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
+	github.com/muesli/reflow v0.3.0 // indirect
+	github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/rivo/uniseg v0.2.0 // indirect
+	github.com/sahilm/fuzzy v0.1.0 // indirect
+	golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
+	golang.org/x/exp/shiny v0.0.0-20220318154914-8dddf5d87bd8 // indirect
+	golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect
+	golang.org/x/mobile v0.0.0-20220307220422-55113b94f09c // indirect
+	golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 // indirect
+	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..5076b1c
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,127 @@
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/bxcodec/faker/v3 v3.8.0 h1:F59Qqnsh0BOtZRC+c4cXoB/VNYDMS3R5mlSpxIap1oU=
+github.com/bxcodec/faker/v3 v3.8.0/go.mod h1:gF31YgnMSMKgkvl+fyEo1xuSMbEuieyqfeslGYFjneM=
+github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho=
+github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
+github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
+github.com/charmbracelet/bubbletea v0.20.0 h1:/b8LEPgCbNr7WWZ2LuE/BV1/r4t5PyYJtDb+J3vpwxc=
+github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM=
+github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
+github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
+github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
+github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
+github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
+github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
+github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
+github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
+github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
+github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
+github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
+github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
+github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
+github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
+github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
+github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
+github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
+github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
+github.com/hajimehoshi/oto v1.0.1 h1:8AMnq0Yr2YmzaiqTg/k1Yzd6IygUGk2we9nmjgbgPn4=
+github.com/hajimehoshi/oto v1.0.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
+github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
+github.com/icza/bitio v1.1.0 h1:ysX4vtldjdi3Ygai5m1cWy4oLkhWTAi+SyO6HC8L9T0=
+github.com/icza/bitio v1.1.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
+github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
+github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
+github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
+github.com/jszwec/csvutil v1.5.1/go.mod h1:Rpu7Uu9giO9subDyMCIQfHVDuLrcaC36UA4YcJjGBkg=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
+github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mewkiz/flac v1.0.7 h1:uIXEjnuXqdRaZttmSFM5v5Ukp4U6orrZsnYGGR3yow8=
+github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
+github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
+github.com/mewkiz/pkg v0.0.0-20211102230744-16a6ce8f1b77 h1:DDyKVkTkrFmd9lR84QW3EIfkkoHlurlpgW+DYuAUJn8=
+github.com/mewkiz/pkg v0.0.0-20211102230744-16a6ce8f1b77/go.mod h1:J/rDzvIiwiVpv72OEP8aJFxLXjGpUdviIIeqJPLIctA=
+github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
+github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
+github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
+github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
+github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
+github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
+github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
+github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
+github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
+github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
+golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
+golang.org/x/exp/shiny v0.0.0-20220318154914-8dddf5d87bd8 h1:EFkPy3Aw0vwtCQVNdU4V0nunADtdtemiTIiz/UMC5rI=
+golang.org/x/exp/shiny v0.0.0-20220318154914-8dddf5d87bd8/go.mod h1:NtXcNtv5Wu0zUbBl574y/D5MMZvnQnV3sgjZxbs64Jo=
+golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4=
+golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mobile v0.0.0-20220307220422-55113b94f09c h1:9J0m/JcA5YXYbamDhF5I3T7cJnR7V75OCLnMCPb5gl4=
+golang.org/x/mobile v0.0.0-20220307220422-55113b94f09c/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 h1:saXMvIOKvRFwbOMicHXr0B1uwoxq9dGmLe5ExMES6c4=
+golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..e72f46f
--- /dev/null
+++ b/main.go
@@ -0,0 +1,15 @@
+package main
+
+import (
+	"log"
+
+	gomus "git.cesium.pw/niku/gomus/pkg"
+	tea "github.com/charmbracelet/bubbletea"
+)
+
+func main() {
+	p := tea.NewProgram(gomus.NewModel())
+	if err := p.Start(); err != nil {
+		log.Fatalf("Failed to start: %v", err)
+	}
+}
diff --git a/pkg/commands.go b/pkg/commands.go
new file mode 100644
index 0000000..3243090
--- /dev/null
+++ b/pkg/commands.go
@@ -0,0 +1,13 @@
+package gomus
+
+import tea "github.com/charmbracelet/bubbletea"
+
+type trackChangeMsg struct {
+	nextTrack track
+}
+
+func newTrackChangeCmd(nextTrack track) tea.Cmd {
+	return func() tea.Msg {
+		return trackChangeMsg{nextTrack}
+	}
+}
diff --git a/pkg/list_delegate.go b/pkg/list_delegate.go
new file mode 100644
index 0000000..d84c9d5
--- /dev/null
+++ b/pkg/list_delegate.go
@@ -0,0 +1,32 @@
+package gomus
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/charmbracelet/bubbles/list"
+	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/lipgloss"
+)
+
+type trackListDelegate struct{}
+
+func (d trackListDelegate) Height() int                               { return 1 }
+func (d trackListDelegate) Spacing() int                              { return 0 }
+func (d trackListDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
+func (d trackListDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
+	t := listItem.(track)
+	f := t.fullName()
+
+	if m.Index() == index {
+		li := lipgloss.NewStyle().Bold(true).Render(f)
+		fmt.Fprintf(w, fmt.Sprintf("[>] %s", li))
+		return
+	}
+
+	fmt.Fprintf(w, f)
+}
+
+func newTrackListDelegate() trackListDelegate {
+	return trackListDelegate{}
+}
diff --git a/pkg/model.go b/pkg/model.go
new file mode 100644
index 0000000..0286adf
--- /dev/null
+++ b/pkg/model.go
@@ -0,0 +1,77 @@
+package gomus
+
+import (
+	"fmt"
+	"os"
+
+	tea "github.com/charmbracelet/bubbletea"
+)
+
+var (
+	termWidth  = 0
+	termHeight = 0
+)
+
+type Model struct {
+	cursor           int
+	currentlyPlaying int
+
+	trackPlayer
+	trackIndex
+
+	trackPlayerView trackPlayerView
+}
+
+func NewModel() Model {
+	if len(os.Args) < 2 {
+		fmt.Println("Expected a path to some music")
+		os.Exit(0)
+	}
+
+	ti := NewDirTrackIndex(os.Args[1])
+	tpv := newTrackPlayerView(ti.tracks)
+
+	return Model{
+		cursor:           0,
+		currentlyPlaying: 0,
+
+		trackIndex:  ti,
+		trackPlayer: trackPlayer{},
+
+		trackPlayerView: tpv,
+	}
+}
+
+func (m Model) Init() tea.Cmd {
+	return tea.Batch(tea.EnterAltScreen, m.trackPlayerView.Init())
+}
+
+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	var cmds []tea.Cmd
+
+	switch msg := msg.(type) {
+	case tea.WindowSizeMsg:
+		termHeight = msg.Height
+		termWidth = msg.Width
+	case tea.KeyMsg:
+		switch msg.String() {
+		case "ctrl+c", "q":
+			m.trackPlayer.close()
+			return m, tea.Quit
+		case "enter":
+			t := m.trackPlayerView.trackList.SelectedItem().(track)
+			cmds = append(cmds, newTrackChangeCmd(t))
+			m.trackPlayer.play(t.getReader())
+		}
+	}
+
+	tpv, cmd := m.trackPlayerView.Update(msg)
+	m.trackPlayerView = tpv
+	cmds = append(cmds, cmd)
+
+	return m, tea.Batch(cmds...)
+}
+
+func (m Model) View() string {
+	return m.trackPlayerView.View()
+}
diff --git a/pkg/status_bar.go b/pkg/status_bar.go
new file mode 100644
index 0000000..4b7a337
--- /dev/null
+++ b/pkg/status_bar.go
@@ -0,0 +1,54 @@
+package gomus
+
+import (
+	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/lipgloss"
+)
+
+var (
+	statusBarStyle = lipgloss.NewStyle().
+			Foreground(lipgloss.AdaptiveColor{Light: "#343433", Dark: "#C1C6B2"}).
+			Background(lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#353533"})
+
+	statusStyle = lipgloss.NewStyle().
+			Inherit(statusBarStyle).
+			Foreground(lipgloss.Color("#FFFDF5")).
+			Background(lipgloss.Color("#FF5F87")).
+			Padding(0, 1).
+			MarginRight(1)
+
+	statusText = lipgloss.NewStyle().Inherit(statusBarStyle)
+)
+
+type statusBar struct {
+	currentTrack track
+}
+
+func (s statusBar) Init() tea.Cmd {
+	return nil
+}
+
+func (s statusBar) View() string {
+	w := lipgloss.Width
+
+	statusKey := statusStyle.Render("NOW PLAYING")
+	statusVal := statusText.Copy().
+		Width(termWidth - w(statusKey)).
+		Render(s.currentTrack.fullName())
+
+	bar := lipgloss.JoinHorizontal(lipgloss.Top,
+		statusKey,
+		statusVal,
+	)
+
+	return statusBarStyle.Width(termWidth).Render(bar)
+}
+
+func (s statusBar) Update(msg tea.Msg) (statusBar, tea.Cmd) {
+	switch msg := msg.(type) {
+	case trackChangeMsg:
+		s.currentTrack = msg.nextTrack
+	}
+
+	return s, nil
+}
diff --git a/pkg/track_fetcher.go b/pkg/track_fetcher.go
new file mode 100644
index 0000000..8371e0c
--- /dev/null
+++ b/pkg/track_fetcher.go
@@ -0,0 +1,72 @@
+package gomus
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/mewkiz/flac"
+	"github.com/mewkiz/flac/meta"
+)
+
+type track struct {
+	name      string
+	artist    string
+	trackPath string
+}
+
+func (t track) FilterValue() string { return "" }
+
+func (t track) fullName() string {
+	return fmt.Sprintf("%s - %s", t.artist, t.name)
+}
+
+func (t track) getReader() io.Reader {
+	f, err := os.Open(t.trackPath)
+	check(err)
+
+	return f
+}
+
+func trackFromFlac(path string) track {
+	s, err := flac.ParseFile(path)
+	check(err)
+
+	t := track{trackPath: path}
+	for _, block := range s.Blocks {
+		if block.Header.Type == meta.TypeVorbisComment {
+			c := block.Body.(*meta.VorbisComment)
+			for _, tag := range c.Tags {
+				if tag[0] == "ARTIST" {
+					t.artist = tag[1]
+				} else if tag[0] == "TITLE" {
+					t.name = tag[1]
+				}
+			}
+		}
+	}
+
+	return t
+}
+
+type trackIndex struct {
+	tracks []track
+}
+
+func NewDirTrackIndex(path string) trackIndex {
+	files, err := ioutil.ReadDir(path)
+	check(err)
+
+	tracks := []track{}
+	for _, file := range files {
+		if !file.IsDir() && strings.HasSuffix(file.Name(), ".flac") {
+			p := filepath.Join(path, file.Name())
+			tracks = append(tracks, trackFromFlac(p))
+		}
+	}
+
+	return trackIndex{tracks: tracks}
+}
diff --git a/pkg/track_player.go b/pkg/track_player.go
new file mode 100644
index 0000000..448497e
--- /dev/null
+++ b/pkg/track_player.go
@@ -0,0 +1,32 @@
+package gomus
+
+import (
+	"io"
+	"time"
+
+	"github.com/faiface/beep"
+	"github.com/faiface/beep/flac"
+	"github.com/faiface/beep/speaker"
+)
+
+type trackPlayer struct {
+	currentStream beep.StreamSeekCloser
+}
+
+func (t trackPlayer) play(reader io.Reader) {
+	if t.currentStream != nil {
+		t.currentStream.Close()
+	}
+	streamer, format, err := flac.Decode(reader)
+	check(err)
+
+	err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
+	check(err)
+
+	speaker.Play(streamer)
+	t.currentStream = streamer
+}
+
+func (t trackPlayer) close() {
+	t.currentStream.Close()
+}
diff --git a/pkg/track_player_view.go b/pkg/track_player_view.go
new file mode 100644
index 0000000..ebf72d1
--- /dev/null
+++ b/pkg/track_player_view.go
@@ -0,0 +1,53 @@
+package gomus
+
+import (
+	"github.com/charmbracelet/bubbles/list"
+	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/lipgloss"
+)
+
+type trackPlayerView struct {
+	trackList list.Model
+	statusBar
+}
+
+func newTrackPlayerView(tracks []track) trackPlayerView {
+	c := mapList(tracks, func(t track) list.Item {
+		return t
+	})
+	l := list.New(c, newTrackListDelegate(), 0, 0)
+	l.SetShowTitle(false)
+	l.SetShowStatusBar(false)
+	l.SetShowHelp(false)
+	l.SetFilteringEnabled(false)
+
+	return trackPlayerView{trackList: l}
+}
+
+func (v trackPlayerView) Init() tea.Cmd {
+	return nil
+}
+
+func (v trackPlayerView) View() string {
+	return lipgloss.JoinVertical(lipgloss.Left, v.trackList.View(), v.statusBar.View())
+}
+
+func (v trackPlayerView) Update(msg tea.Msg) (trackPlayerView, tea.Cmd) {
+	var cmds []tea.Cmd
+
+	switch msg := msg.(type) {
+	case tea.WindowSizeMsg:
+		v.trackList.SetHeight(msg.Height - 2)
+		v.trackList.SetWidth(msg.Width)
+	}
+
+	tl, cmd := v.trackList.Update(msg)
+	v.trackList = tl
+	cmds = append(cmds, cmd)
+
+	sb, cmd := v.statusBar.Update(msg)
+	v.statusBar = sb
+	cmds = append(cmds, cmd)
+
+	return v, tea.Batch(cmds...)
+}
diff --git a/pkg/utils.go b/pkg/utils.go
new file mode 100644
index 0000000..9efda8a
--- /dev/null
+++ b/pkg/utils.go
@@ -0,0 +1,17 @@
+package gomus
+
+import "log"
+
+func mapList[T any, R any](l []T, f func(T) R) []R {
+	var c []R = []R{}
+	for _, item := range l {
+		c = append(c, f(item))
+	}
+	return c
+}
+
+func check(err error) {
+	if err != nil {
+		log.Fatal(err)
+	}
+}