Compare commits

...

62 Commits

Author SHA1 Message Date
e4f23c6805
Deps up
All checks were successful
builder / build (push) Successful in 49s
2024-10-26 14:30:49 -07:00
2d5567621f
rm
All checks were successful
builder / build (push) Successful in 39s
2024-08-27 22:37:59 -07:00
40d0d2c919
deps up
All checks were successful
builder / build (push) Successful in 45s
2024-08-27 09:27:41 -07:00
eb03bcd385 fix(deps): update module modernc.org/sqlite to v1.32.0 (#24)
All checks were successful
builder / build (push) Successful in 47s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) | require | minor | `v1.30.2` -> `v1.32.0` |

---

### Release Notes

<details>
<summary>cznic/sqlite (modernc.org/sqlite)</summary>

### [`v1.32.0`](https://gitlab.com/cznic/sqlite/compare/v1.31.1...v1.32.0)

[Compare Source](https://gitlab.com/cznic/sqlite/compare/v1.31.1...v1.32.0)

### [`v1.31.1`](https://gitlab.com/cznic/sqlite/compare/v1.31.0...v1.31.1)

[Compare Source](https://gitlab.com/cznic/sqlite/compare/v1.31.0...v1.31.1)

### [`v1.31.0`](https://gitlab.com/cznic/sqlite/compare/v1.30.2...v1.31.0)

[Compare Source](https://gitlab.com/cznic/sqlite/compare/v1.30.2...v1.31.0)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzguMCIsInVwZGF0ZWRJblZlciI6IjM4LjIxLjEiLCJ0YXJnZXRCcmFuY2giOiJtYXN0ZXIiLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #24
2024-08-27 16:24:47 +00:00
5c423b082f fix(deps): update github.com/rivo/tview digest to fd649db (#26)
Some checks failed
builder / build (push) Has been cancelled
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [github.com/rivo/tview](https://github.com/rivo/tview) | require | digest | `b0a7293` -> `fd649db` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC44LjMiLCJ1cGRhdGVkSW5WZXIiOiIzOC4zOS40IiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #26
2024-08-27 16:24:33 +00:00
958d214505 fix(deps): update golang.org/x/exp digest to 9b4947d (#31)
Some checks failed
builder / build (push) Has been cancelled
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| golang.org/x/exp | require | digest | `8a7402a` -> `9b4947d` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yMS41IiwidXBkYXRlZEluVmVyIjoiMzguNTEuMCIsInRhcmdldEJyYW5jaCI6Im1hc3RlciIsImxhYmVscyI6W119-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #31
2024-08-27 16:24:06 +00:00
779cb517c9 fix(deps): update module github.com/charmbracelet/bubbles to v0.19.0 (#33)
Some checks failed
builder / build (push) Has been cancelled
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [github.com/charmbracelet/bubbles](https://github.com/charmbracelet/bubbles) | require | minor | `v0.18.0` -> `v0.19.0` |

---

### Release Notes

<details>
<summary>charmbracelet/bubbles (github.com/charmbracelet/bubbles)</summary>

### [`v0.19.0`](https://github.com/charmbracelet/bubbles/releases/tag/v0.19.0)

[Compare Source](https://github.com/charmbracelet/bubbles/compare/v0.18.0...v0.19.0)

### Bugs? Squashed (along with a few nice lil’ features).

Community-Driven Development?! Yep, the majority of the changes in this release were done by the community. *Thank you* all for your contributions that made this release possible.

#### Progress: custom chars

You can now customize the filled and empty characters of the progress bar.

```go
p := progress.New(progress.WithFillCharacters('>', '.'))
```

![progress bar example](https://github.com/user-attachments/assets/ee1a1351-ebee-4f39-8543-af464e60b099)

#### Table improvements

##### Help is on the way

Table now includes a short and full help view so it's easier than ever to tell your users how to interact with the table.

```go
// Render a table with its help.
t := table.New()
view := t.View() + "\n" + t.HelpView()
```

<img src="https://github.com/user-attachments/assets/22195043-5578-4201-982d-a8f9b7eefc5f" width="600">

##### Accessing columns

You can also now get the table's columns (this already existed for rows).

```go
package table

// Columns returns the current columns.
func (m Model) Columns() []Column
```

#### List: page navigation is fixed!

Previously, `list.NextPage()` and `list.PrevPage()` didn't work because the methods did not have pointer receivers. We've fixed this…by making them pointer receivers!

⚠️ Note that this is a minor API change and you *might* need to update your app to pass a pointer receiver to your model rather than a copy. Details in [#&#8203;458](https://github.com/charmbracelet/bubbles/issues/458).

```go
package progress

// NextPage moves to the next page, if available.
func (m *Model) NextPage()

// PrevPage moves to the previous page, if available.
func (m *Model) PrevPage()
```

***

#### What’s Changed

##### Changed

-   Textarea: Improve setting width by [@&#8203;mikelorant](https://github.com/mikelorant) in https://github.com/charmbracelet/bubbles/pull/496
-   Textinput: fix out of range panic if no matched suggestions by [@&#8203;rdnt](https://github.com/rdnt) in https://github.com/charmbracelet/bubbles/pull/473
-   List: Fix no-op list pagination functions by [@&#8203;nekopy](https://github.com/nekopy) in https://github.com/charmbracelet/bubbles/pull/458
-   Table: Clarify position constant in JoinHorizontal by [@&#8203;aditipatelpro](https://github.com/aditipatelpro) in https://github.com/charmbracelet/bubbles/pull/577
-   Progress: make full/empty fill characters configurable by [@&#8203;rwinkhart](https://github.com/rwinkhart) in https://github.com/charmbracelet/bubbles/pull/409
-   Dependencies: switch to x/ansi for text manipulation by [@&#8203;aymanbagabas](https://github.com/aymanbagabas) in https://github.com/charmbracelet/bubbles/pull/505

##### Added

-   Textarea: add help to textarea key bindings by [@&#8203;TravisYeah](https://github.com/TravisYeah) in https://github.com/charmbracelet/bubbles/pull/418
-   Textarea: Add multiline placeholder by [@&#8203;mikelorant](https://github.com/mikelorant) in https://github.com/charmbracelet/bubbles/pull/302
-   Table: Add column return function by [@&#8203;abeleinin](https://github.com/abeleinin) in https://github.com/charmbracelet/bubbles/pull/369
-   Table: Implement help.Keymap interface and add quit mapping by [@&#8203;prgres](https://github.com/prgres) in https://github.com/charmbracelet/bubbles/pull/440
-   Ctrl+Left/Right for WordForward/Backward by [@&#8203;maaslalani](https://github.com/maaslalani) in https://github.com/charmbracelet/bubbles/pull/387
-   Use goreleaser for releases by [@&#8203;aymanbagabas](https://github.com/aymanbagabas) in https://github.com/charmbracelet/bubbles/pull/526

##### Fixed

-   Table: Render Row Tests by [@&#8203;maaslalani](https://github.com/maaslalani) in https://github.com/charmbracelet/bubbles/pull/487
-   Table: Only render columns with a positive width by [@&#8203;fabio42](https://github.com/fabio42) in https://github.com/charmbracelet/bubbles/pull/465
-   Table: Fix inheritence of SelectedStyle in StyleFunc by [@&#8203;gabrielfu](https://github.com/gabrielfu) in https://github.com/charmbracelet/bubbles/pull/539
-   Table: Don't include header height in the total table size by [@&#8203;prgres](https://github.com/prgres) in https://github.com/charmbracelet/bubbles/pull/434
-   Table: Fix premature viewport scroll by [@&#8203;dzeleniak](https://github.com/dzeleniak) in https://github.com/charmbracelet/bubbles/pull/429
-   Textarea: Fix end of buffer character by [@&#8203;mikelorant](https://github.com/mikelorant) in https://github.com/charmbracelet/bubbles/pull/491
-   Textarea: Set textarea default EndOfBufferCharacter to ' ' by [@&#8203;blvrd](https://github.com/blvrd) in https://github.com/charmbracelet/bubbles/pull/510
-   Textarea: End of Buffer alignment by [@&#8203;maaslalani](https://github.com/maaslalani) in https://github.com/charmbracelet/bubbles/pull/486
-   Textinput: don't block input on validation by [@&#8203;GabrielNagy](https://github.com/GabrielNagy) in https://github.com/charmbracelet/bubbles/pull/185
-   Viewport: Fix division by zero in scrollpercentage by [@&#8203;zMoooooritz](https://github.com/zMoooooritz) in https://github.com/charmbracelet/bubbles/pull/494
-   Help: Fix centering by [@&#8203;gabe565](https://github.com/gabe565) in https://github.com/charmbracelet/bubbles/pull/516
-   Progress: Stop spring defaults from overriding WithStringOptions by [@&#8203;nervo](https://github.com/nervo) in https://github.com/charmbracelet/bubbles/pull/540
-   Cursor: Make SetMode method in cursor library handle invalid mode values correctly by [@&#8203;anirudhaCodes](https://github.com/anirudhaCodes) in https://github.com/charmbracelet/bubbles/pull/477

##### Test coverage 

-   Add tests for textarea view by [@&#8203;mikelorant](https://github.com/mikelorant) in https://github.com/charmbracelet/bubbles/pull/485
-   Add tests for paginator by [@&#8203;anirudhaCodes](https://github.com/anirudhaCodes) in https://github.com/charmbracelet/bubbles/pull/480
-   Add tests for textInput Tests by [@&#8203;KevM](https://github.com/KevM) in https://github.com/charmbracelet/bubbles/pull/500
-   Improve textarea tests by [@&#8203;mikelorant](https://github.com/mikelorant) in https://github.com/charmbracelet/bubbles/pull/490

#### New Contributors

-   [@&#8203;rdnt](https://github.com/rdnt) made their first contribution in https://github.com/charmbracelet/bubbles/pull/473
-   [@&#8203;rwinkhart](https://github.com/rwinkhart) made their first contribution in https://github.com/charmbracelet/bubbles/pull/409
-   [@&#8203;mikelorant](https://github.com/mikelorant) made their first contribution in https://github.com/charmbracelet/bubbles/pull/485
-   [@&#8203;anirudhaCodes](https://github.com/anirudhaCodes) made their first contribution in https://github.com/charmbracelet/bubbles/pull/480
-   [@&#8203;nekopy](https://github.com/nekopy) made their first contribution in https://github.com/charmbracelet/bubbles/pull/458
-   [@&#8203;TravisYeah](https://github.com/TravisYeah) made their first contribution in https://github.com/charmbracelet/bubbles/pull/418
-   [@&#8203;abeleinin](https://github.com/abeleinin) made their first contribution in https://github.com/charmbracelet/bubbles/pull/369
-   [@&#8203;fabio42](https://github.com/fabio42) made their first contribution in https://github.com/charmbracelet/bubbles/pull/465
-   [@&#8203;prgres](https://github.com/prgres) made their first contribution in https://github.com/charmbracelet/bubbles/pull/440
-   [@&#8203;zMoooooritz](https://github.com/zMoooooritz) made their first contribution in https://github.com/charmbracelet/bubbles/pull/494
-   [@&#8203;dzeleniak](https://github.com/dzeleniak) made their first contribution in https://github.com/charmbracelet/bubbles/pull/429
-   [@&#8203;KevM](https://github.com/KevM) made their first contribution in https://github.com/charmbracelet/bubbles/pull/500
-   [@&#8203;gabe565](https://github.com/gabe565) made their first contribution in https://github.com/charmbracelet/bubbles/pull/516
-   [@&#8203;blvrd](https://github.com/blvrd) made their first contribution in https://github.com/charmbracelet/bubbles/pull/510
-   [@&#8203;nervo](https://github.com/nervo) made their first contribution in https://github.com/charmbracelet/bubbles/pull/540
-   [@&#8203;gabrielfu](https://github.com/gabrielfu) made their first contribution in https://github.com/charmbracelet/bubbles/pull/539
-   [@&#8203;aditipatelpro](https://github.com/aditipatelpro) made their first contribution in https://github.com/charmbracelet/bubbles/pull/577

**Full Changelog**: https://github.com/charmbracelet/bubbles/compare/v0.18.0...v0.19.0

***

<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg?1" width="400"></a>

Thoughts? Questions? We love hearing from you. Feel free to reach out on [Twitter](https://twitter.com/charmcli), [The Fediverse](https://mastodon.social/@&#8203;charm), or on [Discord](https://charm.sh/chat).

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC40NC4zIiwidXBkYXRlZEluVmVyIjoiMzguNDQuMyIsInRhcmdldEJyYW5jaCI6Im1hc3RlciIsImxhYmVscyI6W119-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #33
2024-08-27 16:23:52 +00:00
1b52748db0
refill
All checks were successful
builder / build (push) Successful in 37s
2024-08-22 00:35:50 -07:00
57c97cddb4
Refill
All checks were successful
builder / build (push) Successful in 47s
2024-08-22 00:17:10 -07:00
e05de59160 fix(deps): update golang.org/x/exp digest to 8a7402a (#23)
All checks were successful
builder / build (push) Successful in 38s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| golang.org/x/exp | require | digest | `e3f2596` -> `8a7402a` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzQuMiIsInVwZGF0ZWRJblZlciI6IjM3LjQzNC4yIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #23
2024-07-20 17:37:50 +00:00
c5e536a472 fix(deps): update golang.org/x/exp digest to e3f2596 (#22)
All checks were successful
builder / build (push) Successful in 38s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| golang.org/x/exp | require | digest | `46b0784` -> `e3f2596` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzIuMCIsInVwZGF0ZWRJblZlciI6IjM3LjQzMi4wIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #22
2024-07-16 20:43:14 +00:00
2c3a2e1778 fix(deps): update module modernc.org/sqlite to v1.30.2 (#21)
All checks were successful
builder / build (push) Successful in 43s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) | require | patch | `v1.30.1` -> `v1.30.2` |

---

### Release Notes

<details>
<summary>cznic/sqlite (modernc.org/sqlite)</summary>

### [`v1.30.2`](https://gitlab.com/cznic/sqlite/compare/v1.30.1...v1.30.2)

[Compare Source](https://gitlab.com/cznic/sqlite/compare/v1.30.1...v1.30.2)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjkuMSIsInVwZGF0ZWRJblZlciI6IjM3LjQyOS4xIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #21
2024-07-14 19:35:29 +00:00
8ce5611154 fix(deps): update module github.com/charmbracelet/lipgloss to v0.12.1 (#20)
Some checks failed
builder / build (push) Has been cancelled
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [github.com/charmbracelet/lipgloss](https://github.com/charmbracelet/lipgloss) | require | minor | `v0.11.1` -> `v0.12.1` |

---

### Release Notes

<details>
<summary>charmbracelet/lipgloss (github.com/charmbracelet/lipgloss)</summary>

### [`v0.12.1`](https://github.com/charmbracelet/lipgloss/releases/tag/v0.12.1)

[Compare Source](https://github.com/charmbracelet/lipgloss/compare/v0.12.0...v0.12.1)

This release fixes a regression with regard to border calculations introduced in Lip Gloss v0.11.1.

***

<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>

Thoughts? Questions? We love hearing from you. Feel free to reach out on [Twitter](https://twitter.com/charmcli), [The Fediverse](https://mastodon.technology/@&#8203;charm), or on [Discord](https://charm.sh/chat).

### [`v0.12.0`](https://github.com/charmbracelet/lipgloss/releases/tag/v0.12.0)

[Compare Source](https://github.com/charmbracelet/lipgloss/compare/v0.11.1...v0.12.0)

### Lists, Check ✓

This release adds a new sub-package for rendering trees and lists.

```go
import "github.com/charmbracelet/lipgloss/list"
```

Define a new list.

```go
l := list.New("A", "B", "C")
```

Print the list.

```go
fmt.Println(l)

// • A
// • B
// • C
```

Lists have the ability to nest.

```go
l := list.New(
  "A", list.New("Artichoke"),
  "B", list.New("Baking Flour", "Bananas", "Barley", "Bean Sprouts"),
  "C", list.New("Cashew Apple", "Cashews", "Coconut Milk", "Curry Paste", "Currywurst"),
  "D", list.New("Dill", "Dragonfruit", "Dried Shrimp"),
  "E", list.New("Eggs"),
  "F", list.New("Fish Cake", "Furikake"),
  "J", list.New("Jicama"),
  "K", list.New("Kohlrabi"),
  "L", list.New("Leeks", "Lentils", "Licorice Root"),
)
```

Print the list.

```go
fmt.Println(l)
```

<p align="center">
<img width="600" alt="image" src="https://github.com/charmbracelet/lipgloss/assets/42545625/0dc9f440-0748-4151-a3b0-7dcf29dfcdb0">
</p>

Lists can be customized via their enumeration function as well as using
`lipgloss.Style`s.

```go
enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("99")).MarginRight(1)
itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212")).MarginRight(1)

l := list.New(
  "Glossier",
  "Claire’s Boutique",
  "Nyx",
  "Mac",
  "Milk",
).
  Enumerator(list.Roman).
  EnumeratorStyle(enumeratorStyle).
  ItemStyle(itemStyle)
```

Print the list.

<p align="center">
<img width="600" alt="List example" src="https://github.com/charmbracelet/lipgloss/assets/42545625/360494f1-57fb-4e13-bc19-0006efe01561">
</p>

In addition to the predefined enumerators (`Arabic`, `Alphabet`, `Roman`, `Bullet`, `Tree`),
you may also define your own custom enumerator:

```go
l := list.New("Duck", "Duck", "Duck", "Duck", "Goose", "Duck", "Duck")

func DuckDuckGooseEnumerator(l list.Items, i int) string {
    if l.At(i).Value() == "Goose" {
        return "Honk →"
    }
    return ""
}

l = l.Enumerator(DuckDuckGooseEnumerator)
```

Print the list:

<p align="center">
<img width="600" alt="image" src="https://github.com/charmbracelet/lipgloss/assets/42545625/157aaf30-140d-4948-9bb4-dfba46e5b87e">
</p>

If you need, you can also build lists incrementally:

```go
l := list.New()

for i := 0; i < repeat; i++ {
    l.Item("Lip Gloss")
}
```

***

<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>

Thoughts? Questions? We love hearing from you. Feel free to reach out on [Twitter](https://twitter.com/charmcli), [The Fediverse](https://mastodon.technology/@&#8203;charm), or on [Discord](https://charm.sh/chat).

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjguMiIsInVwZGF0ZWRJblZlciI6IjM3LjQyOS4xIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #20
2024-07-14 19:35:20 +00:00
4d3f1d85bf fix(deps): update module github.com/lmittmann/tint to v1.0.5 (#19)
All checks were successful
builder / build (push) Successful in 37s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [github.com/lmittmann/tint](https://github.com/lmittmann/tint) | require | patch | `v1.0.4` -> `v1.0.5` |

---

### Release Notes

<details>
<summary>lmittmann/tint (github.com/lmittmann/tint)</summary>

### [`v1.0.5`](https://github.com/lmittmann/tint/releases/tag/v1.0.5)

[Compare Source](https://github.com/lmittmann/tint/compare/v1.0.4...v1.0.5)

#### What's Changed

-   Preserve `tint.Err` attribute key by [@&#8203;kwargs](https://github.com/kwargs) in https://github.com/lmittmann/tint/pull/66

#### New Contributors

-   [@&#8203;kwargs](https://github.com/kwargs) made their first contribution in https://github.com/lmittmann/tint/pull/66

**Full Changelog**: https://github.com/lmittmann/tint/compare/v1.0.4...v1.0.5

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjcuMCIsInVwZGF0ZWRJblZlciI6IjM3LjQyNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #19
2024-07-11 18:25:12 +00:00
5e1dae88e0 fix(deps): update module github.com/charmbracelet/lipgloss to v0.11.1 (#18)
All checks were successful
builder / build (push) Successful in 38s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [github.com/charmbracelet/lipgloss](https://github.com/charmbracelet/lipgloss) | require | patch | `v0.11.0` -> `v0.11.1` |

---

### Release Notes

<details>
<summary>charmbracelet/lipgloss (github.com/charmbracelet/lipgloss)</summary>

### [`v0.11.1`](https://github.com/charmbracelet/lipgloss/releases/tag/v0.11.1)

[Compare Source](https://github.com/charmbracelet/lipgloss/compare/v0.11.0...v0.11.1)

A small patch release to fix text truncation in table cells https://github.com/charmbracelet/lipgloss/issues/324.

#### What's Changed

-   chore: remove deprecated Copy() calls by [@&#8203;meowgorithm](https://github.com/meowgorithm) in https://github.com/charmbracelet/lipgloss/pull/306
-   feat: deprecate Style.ColorWhitespace by [@&#8203;meowgorithm](https://github.com/meowgorithm) in https://github.com/charmbracelet/lipgloss/pull/311
-   feat: deprecate Style.ColorWhitespace by [@&#8203;meowgorithm](https://github.com/meowgorithm) in https://github.com/charmbracelet/lipgloss/pull/314
-   fix: Deprecate UnsetBorderTopBackgroundColor in favor of UnsetBorderTopBackground by [@&#8203;nervo](https://github.com/nervo) in https://github.com/charmbracelet/lipgloss/pull/315

**Full Changelog**: https://github.com/charmbracelet/lipgloss/compare/v0.11.0...v0.11.1

***

<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>

Thoughts? Questions? We love hearing from you. Feel free to reach out on [Twitter](https://twitter.com/charmcli), [The Fediverse](https://mastodon.technology/@&#8203;charm), or [Discord](https://charm.sh/discord).

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjcuMCIsInVwZGF0ZWRJblZlciI6IjM3LjQyNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #18
2024-07-10 19:50:23 +00:00
cceb5fb8bb fix(deps): update module google.golang.org/api to v0.188.0 (#17)
All checks were successful
builder / build (push) Successful in 37s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [google.golang.org/api](https://github.com/googleapis/google-api-go-client) | require | minor | `v0.187.0` -> `v0.188.0` |

---

### Release Notes

<details>
<summary>googleapis/google-api-go-client (google.golang.org/api)</summary>

### [`v0.188.0`](https://github.com/googleapis/google-api-go-client/releases/tag/v0.188.0)

[Compare Source](https://github.com/googleapis/google-api-go-client/compare/v0.187.0...v0.188.0)

##### Features

-   **all:** Auto-regenerate discovery clients ([#&#8203;2665](https://github.com/googleapis/google-api-go-client/issues/2665)) ([e84fa65](e84fa6508e))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2669](https://github.com/googleapis/google-api-go-client/issues/2669)) ([6df3749](6df3749296))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2671](https://github.com/googleapis/google-api-go-client/issues/2671)) ([0d54a85](0d54a85400))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2673](https://github.com/googleapis/google-api-go-client/issues/2673)) ([88240e3](88240e3d98))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2674](https://github.com/googleapis/google-api-go-client/issues/2674)) ([d465cec](d465cec68d))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2675](https://github.com/googleapis/google-api-go-client/issues/2675)) ([a9177bd](a9177bdbdb))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2677](https://github.com/googleapis/google-api-go-client/issues/2677)) ([5dd2fb2](5dd2fb2378))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2678](https://github.com/googleapis/google-api-go-client/issues/2678)) ([d17f6be](d17f6beb5a))

##### Bug Fixes

-   Allow ForceSendFields to work for map types ([#&#8203;2670](https://github.com/googleapis/google-api-go-client/issues/2670)) ([40b5113](40b5113127))
-   Check \[]bytes > 0 instead of nil ([#&#8203;2667](https://github.com/googleapis/google-api-go-client/issues/2667)) ([711eb91](711eb913fe)), refs [#&#8203;2647](https://github.com/googleapis/google-api-go-client/issues/2647)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjYuMiIsInVwZGF0ZWRJblZlciI6IjM3LjQyNi4yIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #17
2024-07-10 00:42:45 +00:00
864c3163b0 fix(deps): update golang.org/x/exp digest to 46b0784 (#16)
All checks were successful
builder / build (push) Successful in 37s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| golang.org/x/exp | require | digest | `7f521ea` -> `46b0784` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjUuMSIsInVwZGF0ZWRJblZlciI6IjM3LjQyNS4xIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #16
2024-07-08 01:48:11 +00:00
2a70db8607
remove imports
All checks were successful
builder / build (push) Successful in 43s
2024-07-06 14:05:01 -07:00
c1b8eee83d fix(deps): update module google.golang.org/api to v0.187.0 (#9)
Some checks failed
builder / build (push) Failing after 18s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [google.golang.org/api](https://github.com/googleapis/google-api-go-client) | require | minor | `v0.182.0` -> `v0.187.0` |

---

### Release Notes

<details>
<summary>googleapis/google-api-go-client (google.golang.org/api)</summary>

### [`v0.187.0`](https://github.com/googleapis/google-api-go-client/releases/tag/v0.187.0)

[Compare Source](https://github.com/googleapis/google-api-go-client/compare/v0.186.0...v0.187.0)

##### Features

-   **all:** Auto-regenerate discovery clients ([#&#8203;2655](https://github.com/googleapis/google-api-go-client/issues/2655)) ([1a28e06](1a28e0622f))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2658](https://github.com/googleapis/google-api-go-client/issues/2658)) ([719f988](719f988502))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2659](https://github.com/googleapis/google-api-go-client/issues/2659)) ([7cd88da](7cd88dabf7))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2660](https://github.com/googleapis/google-api-go-client/issues/2660)) ([3ca2f84](3ca2f844a9))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2661](https://github.com/googleapis/google-api-go-client/issues/2661)) ([0a238f5](0a238f578c))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2663](https://github.com/googleapis/google-api-go-client/issues/2663)) ([6e061ce](6e061ced5f))

##### Bug Fixes

-   **gensupport:** Wrap chunk upload err for retries ([#&#8203;2657](https://github.com/googleapis/google-api-go-client/issues/2657)) ([a758bc1](a758bc17ee))
-   Pass through gRPC api key option to new auth lib ([#&#8203;2664](https://github.com/googleapis/google-api-go-client/issues/2664)) ([e051997](e051997022))

### [`v0.186.0`](https://github.com/googleapis/google-api-go-client/releases/tag/v0.186.0)

[Compare Source](https://github.com/googleapis/google-api-go-client/compare/v0.185.0...v0.186.0)

##### Features

-   **all:** Auto-regenerate discovery clients ([#&#8203;2641](https://github.com/googleapis/google-api-go-client/issues/2641)) ([72fb128](72fb1281b3))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2644](https://github.com/googleapis/google-api-go-client/issues/2644)) ([20ffdd8](20ffdd8000))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2645](https://github.com/googleapis/google-api-go-client/issues/2645)) ([c1a7681](c1a768193e))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2648](https://github.com/googleapis/google-api-go-client/issues/2648)) ([1bac79d](1bac79d78d))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2649](https://github.com/googleapis/google-api-go-client/issues/2649)) ([695484b](695484ba67))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2652](https://github.com/googleapis/google-api-go-client/issues/2652)) ([10c47f3](10c47f3750))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2653](https://github.com/googleapis/google-api-go-client/issues/2653)) ([bc370a7](bc370a705c))

### [`v0.185.0`](https://github.com/googleapis/google-api-go-client/releases/tag/v0.185.0)

[Compare Source](https://github.com/googleapis/google-api-go-client/compare/v0.184.0...v0.185.0)

##### Features

-   **all:** Auto-regenerate discovery clients ([#&#8203;2636](https://github.com/googleapis/google-api-go-client/issues/2636)) ([51ff8a4](51ff8a4794))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2638](https://github.com/googleapis/google-api-go-client/issues/2638)) ([0c868b2](0c868b2608))

##### Bug Fixes

-   **internal/gensupport:** Update shouldRetry for GCS uploads ([#&#8203;2634](https://github.com/googleapis/google-api-go-client/issues/2634)) ([ea513cb](ea513cb749))

### [`v0.184.0`](https://github.com/googleapis/google-api-go-client/releases/tag/v0.184.0)

[Compare Source](https://github.com/googleapis/google-api-go-client/compare/v0.183.0...v0.184.0)

##### Features

-   **all:** Auto-regenerate discovery clients ([#&#8203;2624](https://github.com/googleapis/google-api-go-client/issues/2624)) ([7fccba6](7fccba6a6f))
-   Regen cloudcommerceprocurement v1 from updated discovery file ([#&#8203;2627](https://github.com/googleapis/google-api-go-client/issues/2627)) ([7e30ed2](7e30ed210c))
-   Support structpb.Struct as req/resp ([#&#8203;2632](https://github.com/googleapis/google-api-go-client/issues/2632)) ([ebc44d1](ebc44d1595)), refs [#&#8203;2601](https://github.com/googleapis/google-api-go-client/issues/2601)

##### Bug Fixes

-   **cba:** Update credsNewAuth to support oauth2 over mTLS ([#&#8203;2610](https://github.com/googleapis/google-api-go-client/issues/2610)) ([953f728](953f728941))

### [`v0.183.0`](https://github.com/googleapis/google-api-go-client/releases/tag/v0.183.0)

[Compare Source](https://github.com/googleapis/google-api-go-client/compare/v0.182.0...v0.183.0)

##### Features

-   **all:** Auto-regenerate discovery clients ([#&#8203;2611](https://github.com/googleapis/google-api-go-client/issues/2611)) ([1de148b](1de148b049))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2616](https://github.com/googleapis/google-api-go-client/issues/2616)) ([5f21214](5f21214e22))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2617](https://github.com/googleapis/google-api-go-client/issues/2617)) ([08fdd71](08fdd71cae))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2619](https://github.com/googleapis/google-api-go-client/issues/2619)) ([c7f1614](c7f161413c))
-   **all:** Auto-regenerate discovery clients ([#&#8203;2622](https://github.com/googleapis/google-api-go-client/issues/2622)) ([0077748](007774894a))

##### Bug Fixes

-   Add another temporary dep on genproto ([#&#8203;2614](https://github.com/googleapis/google-api-go-client/issues/2614)) ([4f98211](4f9821115b)), refs [#&#8203;2559](https://github.com/googleapis/google-api-go-client/issues/2559) [#&#8203;2613](https://github.com/googleapis/google-api-go-client/issues/2613)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjQuMyIsInVwZGF0ZWRJblZlciI6IjM3LjQyNC4zIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #9
2024-07-06 20:56:48 +00:00
4549a21b9a fix(deps): update module golang.org/x/oauth2 to v0.21.0 (#8)
Some checks are pending
builder / build (push) Waiting to run
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| golang.org/x/oauth2 | require | minor | `v0.20.0` -> `v0.21.0` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjQuMyIsInVwZGF0ZWRJblZlciI6IjM3LjQyNC4zIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #8
2024-07-06 20:56:39 +00:00
007dcd39e3 chore(deps): update actions/checkout action to v4 (#11)
Some checks are pending
builder / build (push) Waiting to run
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/checkout](https://github.com/actions/checkout) | action | major | `v3` -> `v4` |

---

### Release Notes

<details>
<summary>actions/checkout (actions/checkout)</summary>

### [`v4`](https://github.com/actions/checkout/blob/HEAD/CHANGELOG.md#v417)

[Compare Source](https://github.com/actions/checkout/compare/v3...v4)

-   Bump the minor-npm-dependencies group across 1 directory with 4 updates by [@&#8203;dependabot](https://github.com/dependabot) in https://github.com/actions/checkout/pull/1739
-   Bump actions/checkout from 3 to 4 by [@&#8203;dependabot](https://github.com/dependabot) in https://github.com/actions/checkout/pull/1697
-   Check out other refs/\* by commit by [@&#8203;orhantoy](https://github.com/orhantoy) in https://github.com/actions/checkout/pull/1774
-   Pin actions/checkout's own workflows to a known, good, stable version. by [@&#8203;jww3](https://github.com/jww3) in https://github.com/actions/checkout/pull/1776

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjQuMyIsInVwZGF0ZWRJblZlciI6IjM3LjQyNC4zIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #11
2024-07-06 20:56:31 +00:00
a156f078aa fix(deps): update module gfx.cafe/util/go/fxplus to v1 (#15)
Some checks are pending
builder / build (push) Waiting to run
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| gfx.cafe/util/go/fxplus | require | major | `v0.0.0-20231226111635-bc00a6a250fb` -> `v1.11.1` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjQuMyIsInVwZGF0ZWRJblZlciI6IjM3LjQyNC4zIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #15
2024-07-06 20:56:27 +00:00
58f09618e9 fix(deps): update module gfx.cafe/util/go/frand to v1 (#14)
Some checks failed
builder / build (push) Has been cancelled
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| gfx.cafe/util/go/frand | require | major | `v0.0.0-20231226111635-bc00a6a250fb` -> `v1.11.1` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjQuMyIsInVwZGF0ZWRJblZlciI6IjM3LjQyNC4zIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #14
2024-07-06 20:51:15 +00:00
49880d6831 chore(deps): update goreleaser/goreleaser-action action to v6 (#13)
Some checks are pending
builder / build (push) Waiting to run
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) | action | major | `v5` -> `v6` |

---

### Release Notes

<details>
<summary>goreleaser/goreleaser-action (goreleaser/goreleaser-action)</summary>

### [`v6`](https://github.com/goreleaser/goreleaser-action/compare/v5...v6)

[Compare Source](https://github.com/goreleaser/goreleaser-action/compare/v5...v6)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjQuMyIsInVwZGF0ZWRJblZlciI6IjM3LjQyNC4zIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #13
2024-07-06 20:51:10 +00:00
c3b56d8e7e chore(deps): update actions/setup-go action to v5 (#12)
Some checks are pending
builder / build (push) Waiting to run
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/setup-go](https://github.com/actions/setup-go) | action | major | `v3` -> `v5` |

---

### Release Notes

<details>
<summary>actions/setup-go (actions/setup-go)</summary>

### [`v5`](https://github.com/actions/setup-go/compare/v4...v5)

[Compare Source](https://github.com/actions/setup-go/compare/v4...v5)

### [`v4`](https://github.com/actions/setup-go/compare/v3...v4)

[Compare Source](https://github.com/actions/setup-go/compare/v3...v4)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjQuMyIsInVwZGF0ZWRJblZlciI6IjM3LjQyNC4zIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #12
2024-07-06 20:51:06 +00:00
efc8ebb943 fix(deps): update module modernc.org/sqlite to v1.30.1 (#10)
Some checks are pending
builder / build (push) Waiting to run
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) | require | minor | `v1.29.10` -> `v1.30.1` |

---

### Release Notes

<details>
<summary>cznic/sqlite (modernc.org/sqlite)</summary>

### [`v1.30.1`](https://gitlab.com/cznic/sqlite/compare/v1.30.0...v1.30.1)

[Compare Source](https://gitlab.com/cznic/sqlite/compare/v1.30.0...v1.30.1)

### [`v1.30.0`](https://gitlab.com/cznic/sqlite/compare/v1.29.10...v1.30.0)

[Compare Source](https://gitlab.com/cznic/sqlite/compare/v1.29.10...v1.30.0)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjQuMyIsInVwZGF0ZWRJblZlciI6IjM3LjQyNC4zIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #10
2024-07-06 20:51:00 +00:00
54e6ec39d3 fix(deps): update module golang.org/x/net to v0.27.0 (#7)
Some checks are pending
builder / build (push) Waiting to run
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| golang.org/x/net | require | minor | `v0.25.0` -> `v0.27.0` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjQuMyIsInVwZGF0ZWRJblZlciI6IjM3LjQyNC4zIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #7
2024-07-06 20:50:47 +00:00
5e2327771a fix(deps): update module go.uber.org/fx to v1.22.1 (#6)
Some checks are pending
builder / build (push) Waiting to run
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [go.uber.org/fx](https://github.com/uber-go/fx) | require | patch | `v1.22.0` -> `v1.22.1` |

---

### Release Notes

<details>
<summary>uber-go/fx (go.uber.org/fx)</summary>

### [`v1.22.1`](https://github.com/uber-go/fx/releases/tag/v1.22.1)

[Compare Source](https://github.com/uber-go/fx/compare/v1.22.0...v1.22.1)

##### Fixed

-   Fx apps will only listen to signals when `.Run()`, `.Wait()`, or `.Done()`
    are called, fixing a regression introduced in v1.19.0.

Thank you [@&#8203;MarcoPolo](https://github.com/MarcoPolo) for your contribution to the release.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjQuMyIsInVwZGF0ZWRJblZlciI6IjM3LjQyNC4zIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #6
2024-07-06 20:50:41 +00:00
2760c3ae93 fix(deps): update github.com/rivo/tview digest to b0a7293 (#5)
Some checks are pending
builder / build (push) Waiting to run
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [github.com/rivo/tview](https://github.com/rivo/tview) | require | digest | `037df49` -> `b0a7293` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjQuMyIsInVwZGF0ZWRJblZlciI6IjM3LjQyNC4zIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #5
2024-07-06 20:50:37 +00:00
8347c24fbe Update renovate.json
All checks were successful
builder / build (push) Successful in 35s
2024-07-06 20:33:53 +00:00
a90f7d2ce9 chore(deps): update golang.org/x/exp digest to 7f521ea (#2)
All checks were successful
builder / build (push) Successful in 44s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| golang.org/x/exp | require | digest | `404ba88` -> `7f521ea` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjQuMyIsInVwZGF0ZWRJblZlciI6IjM3LjQyNC4zIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #2
2024-07-06 20:26:16 +00:00
4d4038fd99 chore(deps): update module github.com/charmbracelet/bubbletea to v0.26.6 (#3)
Some checks failed
builder / build (push) Has been cancelled
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) | require | patch | `v0.26.4` -> `v0.26.6` |

---

### Release Notes

<details>
<summary>charmbracelet/bubbletea (github.com/charmbracelet/bubbletea)</summary>

### [`v0.26.6`](https://github.com/charmbracelet/bubbletea/releases/tag/v0.26.6)

[Compare Source](https://github.com/charmbracelet/bubbletea/compare/v0.26.5...v0.26.6)

#### Changelog

##### Bug fixes

-   [`60a57ea`](60a57eaf1f): fix: nil deref on release terminal ([@&#8203;aymanbagabas](https://github.com/aymanbagabas))

***

<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>

Thoughts? Questions? We love hearing from you. Feel free to reach out on [Twitter](https://twitter.com/charmcli), [The Fediverse](https://mastodon.technology/@&#8203;charm), or on [Discord](https://charm.sh/chat).

### [`v0.26.5`](https://github.com/charmbracelet/bubbletea/releases/tag/v0.26.5)

[Compare Source](https://github.com/charmbracelet/bubbletea/compare/v0.26.4...v0.26.5)

Fix special keys input handling on Windows using the latest Windows Console Input driver.

#### Changelog

##### New Features

-   [`42a7dd8`](42a7dd8f89): feat(ci): use goreleaser for releases ([#&#8203;1023](https://github.com/charmbracelet/bubbletea/issues/1023)) ([@&#8203;aymanbagabas](https://github.com/aymanbagabas))

##### Bug fixes

-   [`a08802e`](a08802ea9f): fix(windows): coninput not handling control sequences ([#&#8203;1041](https://github.com/charmbracelet/bubbletea/issues/1041)) ([@&#8203;Sculas](https://github.com/Sculas))

##### Other work

-   [`2d65ed6`](2d65ed65a3): chore(examples): removed use of deprecated Copy ([@&#8203;arianizadi](https://github.com/arianizadi))

***

<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>

Thoughts? Questions? We love hearing from you. Feel free to reach out on [Twitter](https://twitter.com/charmcli), [The Fediverse](https://mastodon.technology/@&#8203;charm), or on [Discord](https://charm.sh/chat).

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MjQuMyIsInVwZGF0ZWRJblZlciI6IjM3LjQyNC4zIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #3
2024-07-06 20:26:00 +00:00
5e40c8e2a2 chore: Configure Renovate (#1)
All checks were successful
builder / build (push) Successful in 36s
Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboarding PR to help you understand and configure settings before regular Pull Requests begin.

🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged.

---
### Detected Package Files

 * `.gitea/workflows/push.yaml` (github-actions)
 * `.gitea/workflows/releaser.yaml` (github-actions)
 * `go.mod` (gomod)

### What to Expect

With your current configuration, Renovate will create 9 Pull Requests:

<details>
<summary>chore(deps): update golang.org/x/exp digest to 7f521ea</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/golang.org-x-exp-digest`
  - Merge into: `master`
  - Upgrade golang.org/x/exp to `7f521ea00fb8`

</details>

<details>
<summary>chore(deps): update module github.com/charmbracelet/bubbletea to v0.26.6</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/github.com-charmbracelet-bubbletea-0.x`
  - Merge into: `master`
  - Upgrade [github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea) to `v0.26.6`

</details>

<details>
<summary>chore(deps): update module go.uber.org/fx to v1.22.1</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/go.uber.org-fx-1.x`
  - Merge into: `master`
  - Upgrade [go.uber.org/fx](https://github.com/uber-go/fx) to `v1.22.1`

</details>

<details>
<summary>chore(deps): update module golang.org/x/net to v0.27.0</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/golang.org-x-net-0.x`
  - Merge into: `master`
  - Upgrade golang.org/x/net to `v0.27.0`

</details>

<details>
<summary>chore(deps): update module golang.org/x/oauth2 to v0.21.0</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/golang.org-x-oauth2-0.x`
  - Merge into: `master`
  - Upgrade golang.org/x/oauth2 to `v0.21.0`

</details>

<details>
<summary>chore(deps): update module google.golang.org/api to v0.187.0</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/google.golang.org-api-0.x`
  - Merge into: `master`
  - Upgrade [google.golang.org/api](https://github.com/googleapis/google-api-go-client) to `v0.187.0`

</details>

<details>
<summary>chore(deps): update module modernc.org/sqlite to v1.30.1</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/modernc.org-sqlite-1.x`
  - Merge into: `master`
  - Upgrade [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) to `v1.30.1`

</details>

<details>
<summary>chore(deps): update module gfx.cafe/util/go/frand to v1</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/gfx.cafe-util-go-frand-1.x`
  - Merge into: `master`
  - Upgrade gfx.cafe/util/go/frand to `5db68942734eba89e6f024a5d644d0b787c2587a`

</details>

<details>
<summary>chore(deps): update module gfx.cafe/util/go/fxplus to v1</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/gfx.cafe-util-go-fxplus-1.x`
  - Merge into: `master`
  - Upgrade gfx.cafe/util/go/fxplus to `5db68942734eba89e6f024a5d644d0b787c2587a`

</details>

🚸 Branch creation will be limited to maximum 2 per hour, so it doesn't swamp any CI resources or overwhelm the project. See docs for `prhourlylimit` for details.

---

 Got questions? Check out Renovate's [Docs](https://docs.renovatebot.com/), particularly the Getting Started section.
If you need any further assistance then you can also [request help here](https://github.com/renovatebot/renovate/discussions).

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).

<!--renovate-config-hash:94693a990c975907e7f13da3309b9d56ba02b3983519b41786edf5cf031e457c-->

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Reviewed-on: #1
2024-07-06 20:11:10 +00:00
2a28c74caf
tview
All checks were successful
builder / build (push) Successful in 38s
2024-05-31 18:16:20 -07:00
c1ac26e684
update
All checks were successful
builder / build (push) Successful in 1m17s
2024-05-30 15:19:52 -07:00
8a21c7cb52
error out on invalid arguments
All checks were successful
builder / build (push) Successful in 1m10s
deployer / go-releaser (push) Successful in 2m3s
2024-04-07 10:43:58 -07:00
a7152075fa
job names
All checks were successful
builder / build (push) Successful in 23s
deployer / go-releaser (push) Successful in 55s
2024-04-06 11:17:37 -07:00
c28448c295
Gitea token
All checks were successful
build-binary / Build (push) Successful in 23s
deployer / Build (push) Successful in 1m1s
2024-04-06 10:48:25 -07:00
ec69919ec6
Gitea token
All checks were successful
build-binary / Build (push) Successful in 21s
deployer / Build (push) Successful in 1m41s
2024-04-06 10:45:34 -07:00
dd2284770d
remove
Some checks failed
build-binary / Build (push) Successful in 1m1s
deployer / Build (push) Failing after 25s
2024-04-06 10:42:15 -07:00
52dbe52c50
maybe
Some checks failed
build-binary / Build (push) Has been cancelled
ci/woodpecker/push/woodpecker Pipeline was successful
2024-04-06 10:41:58 -07:00
bdec590110
build
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Build / Build (push) Successful in 1m23s
2024-04-06 10:28:14 -07:00
b516cb8d2f
build
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
Build / Build (push) Failing after 2s
2024-04-06 09:46:14 -07:00
bef9898bf2
better completions
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-04-02 21:30:26 -07:00
786ee38671
build
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-03-29 18:40:06 -07:00
823774f7bd
dependency bump 2024-03-29 16:49:10 -07:00
ba07377ca8
chore: dep updates 2024-03-26 12:23:17 -07:00
dc2c74dd4b
move
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-02-23 12:48:32 -08:00
c26d11299a
make border full terminal size
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-02-21 22:13:09 -08:00
c1d4a0b5e0
fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-02-20 18:58:40 -08:00
338bef9f75
toggle
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/tag/woodpecker Pipeline failed
2024-02-20 09:00:57 -08:00
bafaa8a941
make
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-02-19 18:00:40 -08:00
e6344bef01
cleanup
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-02-19 17:58:05 -08:00
ae4291032e
license
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-19 17:21:17 -08:00
ad77e043d6
readme
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-19 13:33:36 -08:00
2116052d5e
auto retry
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-19 10:26:44 -08:00
1faea3fd7f
v3
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-19 10:11:47 -08:00
f6f7d3bd70
get client only when needed
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-02-18 23:33:32 -08:00
86c2c38dfe
log settings
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-02-18 22:17:28 -08:00
59fff8dfef
cleanup
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-02-18 20:00:15 -08:00
8e067ac28d
nevermind
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-02-18 19:04:37 -08:00
c474690f3e
releaser
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline failed
2024-02-18 19:02:05 -08:00
48 changed files with 1225 additions and 685 deletions

View File

@ -0,0 +1,15 @@
name: builder
run-name: ${{ gitea.actor }} is building
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22"
cache: true
- run: go mod tidy
- run: go build -o gspot

View File

@ -0,0 +1,28 @@
name: deployer
run-name: ${{ gitea.actor }} is releasing
on:
push:
tags:
- "*"
jobs:
go-releaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup up go
uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: nightly
args: release --clean
env:
GITEA_TOKEN: ${{ secrets.ACCESS_TOKEN_GITEA}}

82
.golangci.yml Normal file
View File

@ -0,0 +1,82 @@
linters:
disable-all: true
enable:
- gofmt
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- gocritic
- bodyclose
- gosec
- prealloc
- unconvert
- unused
linters-settings:
gocritic:
# Which checks should be enabled; can't be combined with 'disabled-checks';
# See https://go-critic.github.io/overview#checks-overview
# To check which checks are enabled run `GL_DEBUG=gocritic ./build/bin/golangci-lint run`
# By default list of stable checks is used.
enabled-checks:
- ruleguard
- truncateCmp
# Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty
disabled-checks:
- captLocal
- assignOp
- paramTypeCombine
- importShadow
- commentFormatting
# Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks.
# Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags".
enabled-tags:
- performance
- diagnostic
- opinionated
disabled-tags:
- experimental
settings:
hugeParam:
# size in bytes that makes the warning trigger (default 80)
sizeThreshold: 1000
rangeValCopy:
sizeThreshold: 1024
rangeExprCopy:
# size in bytes that makes the warning trigger (default 512)
sizeThreshold: 512
# whether to check test functions (default true)
skipTestFuncs: true
truncateCmp:
# whether to skip int/uint/uintptr types (default true)
skipArchDependent: true
underef:
# whether to skip (*x).method() calls where x is a pointer receiver (default true)
skipRecvDeref: true
govet:
disable:
- deepequalerrors
- fieldalignment
- shadow
- unsafeptr
goconst:
min-len: 2
min-occurrences: 2
issues:
exclude-rules:
- linters:
- golint
text: "should be"
- linters:
- errcheck
text: "not checked"
- linters:
- staticcheck
text: "SA(1019|1029|5011)"

View File

@ -30,31 +30,29 @@ archives:
{{- else }}{{ .Arch }}{{ end }} {{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }}
format_overrides: format_overrides:
- goos: windows - goos: windows
format: zip format: zip
files:
- completions/*
checksum: checksum:
name_template: 'checksums.txt' name_template: "checksums.txt"
snapshot: snapshot:
name_template: "{{ incpatch .Version }}-next" name_template: "{{ incpatch .Version }}-next"
changelog: changelog:
sort: asc sort: asc
groups: groups:
- title: Added - title: Added
regexp: '^.*?ADD(\([[:word:]]+\))??!?:.+$' regexp: '^.*?ADD(\([[:word:]]+\))??!?:.+$'
order: 0 order: 0
- title: 'Bug fixes' - title: "Bug fixes"
regexp: '^.*?BUG(\([[:word:]]+\))??!?:.+$' regexp: '^.*?BUG(\([[:word:]]+\))??!?:.+$'
order: 1 order: 1
- title: 'Enhancements' - title: "Enhancements"
regexp: '^.*?IMPROVED(\([[:word:]]+\))??!?:.+$' regexp: '^.*?IMPROVED(\([[:word:]]+\))??!?:.+$'
order: 1 order: 1
- title: 'Docs' - title: "Docs"
regexp: '^.*?DOC(\([[:word:]]+\))??!?:.+$' regexp: '^.*?DOC(\([[:word:]]+\))??!?:.+$'
order: 1 order: 1
- title: 'CI' - title: "CI"
regexp: '^.*?CI(\([[:word:]]+\))??!?:.+$' regexp: '^.*?CI(\([[:word:]]+\))??!?:.+$'
order: 1 order: 1
- title: Others - title: Others
order: 999 order: 999

View File

@ -1,14 +0,0 @@
steps:
build:
image: golang:1.22
commands:
- go mod tidy
- go build -o gspot
publish:
image: goreleaser/goreleaser:nightly
commands:
- goreleaser release --clean
secrets: [ gitea_token ]
when:
event: tag

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 abs3nt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,16 +1,14 @@
build: gspot build:
gspot: $(shell find . -name '*.go')
go build -ldflags="-X 'git.asdf.cafe/abs3nt/gspot/src/components/cli.Version=$(shell git show -s --date=short --pretty='format:%h (%ad)' HEAD)'" -o dist/ . go build -ldflags="-X 'git.asdf.cafe/abs3nt/gspot/src/components/cli.Version=$(shell git show -s --date=short --pretty='format:%h (%ad)' HEAD)'" -o dist/ .
run: run: build
go run main.go ./dist/gspot
tidy: tidy:
go mod tidy go mod tidy
clean: clean:
rm -rf bin rm -rf dist
uninstall: uninstall:
rm -f /usr/bin/gspot rm -f /usr/bin/gspot

82
README.md Normal file
View File

@ -0,0 +1,82 @@
IF YOU ARE ON GITHUB.COM GO HERE INSTEAD: https://git.asdf.cafe/abs3nt/gspot
If you open an issue or PR on github I won't see it please use git.asdf.cafe. Register on asdf and open your PRs there
This project is still under heavy development and some things might not work or not work as intended. Don't hesitate to open an issue to let me know.
---
[![status-badge](https://ci.asdf.cafe/api/badges/abs3nt/gspot/status.svg)](https://ci.asdf.cafe/abs3nt/gspot)
# To install (with a package manager):
## Archlinux ([AUR])
`yay -S gspot-git`
# To build from source by pulling and building the binary
`git clone https://git.asdf.cafe/abs3nt/gspot`
`cd gspot`
`make build && sudo make install`
[AUR]: https://aur.archlinux.org/packages/gspot-git
# Configuration
go here https://developer.spotify.com/dashboard/applications to make a spotify application. you will need a client ID and a client secret. Set your redirect uri like this:
`http://localhost:8888/callback`
add your information to ~/.config/gspot/gspot.yml like this
```
client_id: "idgoeshere"
client_secret: "secretgoeshere"
port: "8888"
```
if you dont want to store your secret in the file in plaintext you can use a command to retreive it:
```
client_secret_cmd: "secret spotify_secret"
```
you should have either client_secret or client_secret_cmd
you can enable debug logging by adding
```
log_level: "debug"
log_output: "file"
```
it will log to ~/.config/gspot/gspot.log
## RUNNING
`gspot`
you will be asked to login, you will only have to do this the first time. After login you will be asked to select your default device.
helpful keybinds are shown in the bottom of the screen, hit ? to see all of them
To use the custom radio feature:
`gspot radio`
or hit ctrl+r on any track in the TUI. This will start an extended radio. To replenish the current radio run `gspot refillradio` and all the songs already listened will be removed and that number of new recomendations will be added.
This radio uses slightly different logic than the standard spotify radio to give a longer playlist and more recomendation. With a cronjob you can schedule refill to run to have an infinite and morphing radio station.
To view help:
`gspot --help`
Very open to contributations feel free to open a PR
[tmux plugin](https://git.asdf.cafe/abs3nt/tmux-gspot)
[wiki](https://git.asdf.cafe/abs3nt/gspot/wiki)

View File

@ -1,20 +1,16 @@
#compdef gspot #compdef gspot
_cli_zsh_autocomplete() { local -a opts
local -a opts local cur
local cur cur=${words[-1]}
cur=${words[-1]} if [[ "$cur" == "-"* ]]; then
if [[ "$cur" == "-"* ]]; then opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-shell-completion)}")
opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}") else
else opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-shell-completion)}")
opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-bash-completion)}") fi
fi
if [[ "${opts[1]}" != "" ]]; then if [[ "${opts[1]}" != "" ]]; then
_describe 'values' opts _describe 'values' opts
else else
_files _files
fi fi
}
compdef _cli_zsh_autocomplete gspot

106
go.mod
View File

@ -1,79 +1,81 @@
module git.asdf.cafe/abs3nt/gspot module git.asdf.cafe/abs3nt/gspot
go 1.22.0 go 1.22.3
require ( require (
gfx.cafe/util/go/frand v0.0.0-20231226111635-bc00a6a250fb
gfx.cafe/util/go/fxplus v0.0.0-20231226111635-bc00a6a250fb
git.asdf.cafe/abs3nt/gunner v0.0.1 git.asdf.cafe/abs3nt/gunner v0.0.1
github.com/atotto/clipboard v0.1.4 github.com/atotto/clipboard v0.1.4
github.com/charmbracelet/bubbles v0.18.0 github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/bubbletea v0.25.0 github.com/charmbracelet/bubbletea v1.1.2
github.com/charmbracelet/lipgloss v0.9.1 github.com/charmbracelet/lipgloss v0.13.1
github.com/lmittmann/tint v1.0.4 github.com/lmittmann/tint v1.0.5
github.com/urfave/cli/v2 v2.27.1 github.com/rivo/tview v0.0.0-20241016194538-c5e4fb24af13
github.com/zmb3/spotify/v2 v2.4.1 github.com/urfave/cli/v3 v3.0.0-alpha9.1
go.uber.org/fx v1.20.1 github.com/zmb3/spotify/v2 v2.4.2
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a go.uber.org/fx v1.23.0
golang.org/x/net v0.17.0 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
golang.org/x/oauth2 v0.10.0 golang.org/x/net v0.30.0
golang.org/x/sync v0.3.0 golang.org/x/oauth2 v0.23.0
google.golang.org/api v0.133.0 golang.org/x/sync v0.8.0
modernc.org/sqlite v1.29.1 google.golang.org/api v0.203.0
modernc.org/sqlite v1.33.1
) )
require ( require (
cloud.google.com/go/compute v1.22.0 // indirect cloud.google.com/go/auth v0.9.9 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect cloud.google.com/go/compute/metadata v0.5.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/charmbracelet/x/ansi v0.4.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/charmbracelet/x/term v0.2.0 // indirect
github.com/cristalhq/aconfig v0.18.5 // indirect github.com/cristalhq/aconfig v0.18.6 // indirect
github.com/cristalhq/aconfig/aconfigdotenv v0.17.1 // indirect github.com/cristalhq/aconfig/aconfigdotenv v0.17.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gdamore/encoding v1.0.1 // indirect
github.com/gdamore/tcell/v2 v2.7.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/google/s2a-go v0.1.8 // indirect
github.com/google/s2a-go v0.1.4 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/joho/godotenv v1.4.0 // indirect github.com/joho/godotenv v1.5.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect github.com/muesli/termenv v0.15.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7-0.20240127222946-601bbb3750c2 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
go.uber.org/atomic v1.7.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
go.uber.org/dig v1.17.0 // indirect go.opentelemetry.io/otel v1.31.0 // indirect
go.uber.org/multierr v1.6.0 // indirect go.opentelemetry.io/otel/metric v1.31.0 // indirect
go.uber.org/zap v1.23.0 // indirect go.opentelemetry.io/otel/trace v1.31.0 // indirect
golang.org/x/crypto v0.14.0 // indirect go.uber.org/dig v1.18.0 // indirect
golang.org/x/sys v0.16.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/term v0.13.0 // indirect go.uber.org/zap v1.27.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/crypto v0.28.0 // indirect
google.golang.org/appengine v1.6.7 // indirect golang.org/x/sys v0.26.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230725213213-b022f6e96895 // indirect golang.org/x/term v0.25.0 // indirect
google.golang.org/grpc v1.56.2 // indirect golang.org/x/text v0.19.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect google.golang.org/grpc v1.67.1 // indirect
modernc.org/libc v1.41.0 // indirect google.golang.org/protobuf v1.35.1 // indirect
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 // indirect
modernc.org/libc v1.61.0 // indirect
modernc.org/mathutil v1.6.0 // indirect modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect modernc.org/token v1.1.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect

296
go.sum
View File

@ -13,16 +13,18 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/auth v0.9.9 h1:BmtbpNQozo8ZwW2t7QJjnrQtdganSdmqeIBxHxNkEZQ=
cloud.google.com/go/auth v0.9.9/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI=
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute v1.22.0 h1:cB8R6FtUtT1TYGl5R3xuxnW6OUIc/DrT2aiR16TTG7Y= cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
cloud.google.com/go/compute v1.22.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
@ -35,50 +37,35 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gfx.cafe/util/go/frand v0.0.0-20231226111635-bc00a6a250fb h1:sBsE/GNN43F9a9/FQjbUHMNm3YXLe+YDOzuEJ0e2w0Y=
gfx.cafe/util/go/frand v0.0.0-20231226111635-bc00a6a250fb/go.mod h1:LNHxMJl0WnIr5+OChYxlVopxk+j7qxZv0XvWCzB6uGE=
gfx.cafe/util/go/fxplus v0.0.0-20231226111635-bc00a6a250fb h1:JL2ZB1wCxGS/mBIsTwDrgGEndHVQtZuba9dgSk+OsSE=
gfx.cafe/util/go/fxplus v0.0.0-20231226111635-bc00a6a250fb/go.mod h1:qcgf/NcKZwJCETErwNtofMa10hQtP28ec1bN2nl8ahA=
git.asdf.cafe/abs3nt/gunner v0.0.1 h1:N6kCe7fH83kzm1Sjp/5uZbl8FM5s7KoYCfmhO8qyQbA= git.asdf.cafe/abs3nt/gunner v0.0.1 h1:N6kCe7fH83kzm1Sjp/5uZbl8FM5s7KoYCfmhO8qyQbA=
git.asdf.cafe/abs3nt/gunner v0.0.1/go.mod h1:Q4zhiPfmffCVAb5xIzZn6Momm91uf/deqRVd2/vdjd4= git.asdf.cafe/abs3nt/gunner v0.0.1/go.mod h1:Q4zhiPfmffCVAb5xIzZn6Momm91uf/deqRVd2/vdjd4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= github.com/charmbracelet/bubbletea v1.1.2 h1:naQXF2laRxyLyil/i7fxdpiz1/k06IKquhm4vBfHsIc=
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= github.com/charmbracelet/bubbletea v1.1.2/go.mod h1:9HIU/hBV24qKjlehyj8z1r/tR9TYTQEag+cWZnuXo8E=
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= github.com/charmbracelet/lipgloss v0.13.1 h1:Oik/oqDTMVA01GetT4JdEC033dNzWoQHdWnHnQmXE2A=
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= github.com/charmbracelet/lipgloss v0.13.1/go.mod h1:zaYVJ2xKSKEnTEEbX6uAHabh2d975RJ+0yfkFpRBz5U=
github.com/charmbracelet/x/ansi v0.4.0 h1:NqwHA4B23VwsDn4H3VcNX1W1tOmgnvY1NDx5tOXdnOU=
github.com/charmbracelet/x/ansi v0.4.0/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cristalhq/aconfig v0.17.0/go.mod h1:NXaRp+1e6bkO4dJn+wZ71xyaihMDYPtCSvEhMTm/H3E= github.com/cristalhq/aconfig v0.17.0/go.mod h1:NXaRp+1e6bkO4dJn+wZ71xyaihMDYPtCSvEhMTm/H3E=
github.com/cristalhq/aconfig v0.18.5 h1:QqXH/Gy2c4QUQJTV2BN8UAuL/rqZ3IwhvxeC8OgzquA= github.com/cristalhq/aconfig v0.18.6 h1:8KRBznzdjUUiaa7HeIpYbMx1uPE1/xOBEU1ajsnmNME=
github.com/cristalhq/aconfig v0.18.5/go.mod h1:NXaRp+1e6bkO4dJn+wZ71xyaihMDYPtCSvEhMTm/H3E= github.com/cristalhq/aconfig v0.18.6/go.mod h1:9ogrGEt9yU5V4pif/ThkVUfhj8JkdV+iDeahZGgfnDU=
github.com/cristalhq/aconfig/aconfigdotenv v0.17.1 h1:HG2ql5fGe4FLL2fUv6o+o0YRyF1mWEcYkNfWGWD82k4= github.com/cristalhq/aconfig/aconfigdotenv v0.17.1 h1:HG2ql5fGe4FLL2fUv6o+o0YRyF1mWEcYkNfWGWD82k4=
github.com/cristalhq/aconfig/aconfigdotenv v0.17.1/go.mod h1:gQIKkh+HkVcODvMNz/cLbH65Pk9b0r4tfolCOsI8G9I= github.com/cristalhq/aconfig/aconfigdotenv v0.17.1/go.mod h1:gQIKkh+HkVcODvMNz/cLbH65Pk9b0r4tfolCOsI8G9I=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -89,13 +76,24 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -125,8 +123,6 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -138,8 +134,9 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@ -149,100 +146,92 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM= github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w= github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc= github.com/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw=
github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 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/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
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.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/tview v0.0.0-20241016194538-c5e4fb24af13 h1:SG5LUOAzLU9svb9HTLJI2WnLHQDEe86fXWJ4h2fQg0s=
github.com/rivo/tview v0.0.0-20241016194538-c5e4fb24af13/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7-0.20240127222946-601bbb3750c2 h1:tcc3ZFBvjydcgrAxavZRYqFqCKzy0FJ+UY4ATq4QVXk= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7-0.20240127222946-601bbb3750c2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= github.com/urfave/cli/v3 v3.0.0-alpha9.1 h1:1fJU+bltkwN8lF4Sni/X0i1d8XwPIrS82ivZ8qsp/q4=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/urfave/cli/v3 v3.0.0-alpha9.1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zmb3/spotify/v2 v2.4.1 h1:2ENzO3XQLOQBuxgT1Z9+PlCBSkjNgzFzmRaPns0tjM4= github.com/zmb3/spotify/v2 v2.4.2 h1:j3yNN5lKVEMZQItJF4MHCSZbfNWmXO+KaC+3RFaLlLc=
github.com/zmb3/spotify/v2 v2.4.1/go.mod h1:p3r7mCCxHepzVaJOe3w1dlx9SL+T8iiQR14tfXJpuTE= github.com/zmb3/spotify/v2 v2.4.2/go.mod h1:XOV7BrThayFYB9AAfB+L0Q0wyxBuLCARk4fI/ZXCBW8=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@ -250,28 +239,34 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI= go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU= go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
go.uber.org/fx v1.20.1 h1:zVwVQGS8zYvhh9Xxcu4w1M6ESyeMzebzj2NbSayZ4Mk= go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
go.uber.org/fx v1.20.1/go.mod h1:iSYNbHf2y55acNCwCXKx7LbWb5WG1Bnue5RDXz1OREg= go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg=
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU=
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -282,8 +277,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/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-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -306,8 +301,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -336,20 +331,21 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -360,8 +356,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -389,35 +385,36 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -463,8 +460,8 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -485,15 +482,14 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.133.0 h1:N7Ym5Hl0Dpn0I0o7R1z4UpVA1GCDyS8vbPu1/ObV73A= google.golang.org/api v0.203.0 h1:SrEeuwU3S11Wlscsn+LA1kb/Y5xT8uggJSkIhD08NAU=
google.golang.org/api v0.133.0/go.mod h1:sjRL3UnjTx5UqNQS9EWr9N8p7xbHpy1k0XGRLCf3Spk= google.golang.org/api v0.203.0/go.mod h1:BuOVyCSYEPwJb3npWvDnNmFI92f3GeRnHNkETneT3SI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@ -518,19 +514,17 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 h1:Au6te5hbKUV8pIYWHqOUZ1pva5qK/rwbIhoXEUB9Lu8= google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 h1:Df6WuGvthPzc+JiQ/G+m+sNX24kc0aTBqoDN/0yyykE=
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:O9kGHb51iE/nOGvQaDUuadVYqovW56s5emA88lQnj6Y= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 h1:XVeBY8d/FaK4848myy41HBqnDwvxeV3zMZhwN1TvAMU= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230725213213-b022f6e96895 h1:co8AMhI481nhd3WBfW2mq5msyQHNBcGn7G9GCEqz45k= google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230725213213-b022f6e96895/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -543,12 +537,9 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI=
google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -561,15 +552,14 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@ -580,16 +570,28 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk= modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY= modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 h1:IYXPPTTjjoSHvUClZIYexDiO7g+4x+XveKT4gCIAwiY=
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/sqlite v1.29.1 h1:19GY2qvWB4VPw0HppFlZCPAbmxFU41r+qjKZQdQ1ryA= modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/sqlite v1.29.1/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

50
main.go
View File

@ -1,22 +1,36 @@
package main package main
import ( import (
"go.uber.org/fx" "context"
"errors"
"fmt"
"log/slog"
"go.uber.org/fx"
"go.uber.org/fx/fxevent"
"git.asdf.cafe/abs3nt/gspot/src/app"
"git.asdf.cafe/abs3nt/gspot/src/components/cache" "git.asdf.cafe/abs3nt/gspot/src/components/cache"
"git.asdf.cafe/abs3nt/gspot/src/components/cli" "git.asdf.cafe/abs3nt/gspot/src/components/cli"
"git.asdf.cafe/abs3nt/gspot/src/components/commands" "git.asdf.cafe/abs3nt/gspot/src/components/commands"
"git.asdf.cafe/abs3nt/gspot/src/components/logger"
"git.asdf.cafe/abs3nt/gspot/src/services"
) )
func main() { func main() {
var s fx.Shutdowner var s fx.Shutdowner
app := fx.New( app := fx.New(
fx.WithLogger(func(logger *slog.Logger) fxevent.Logger {
l := &fxevent.SlogLogger{Logger: logger}
l.UseLogLevel(slog.LevelDebug)
return l
}),
fx.Populate(&s), fx.Populate(&s),
app.Config, services.Config,
fx.Provide( fx.Provide(
Context,
cache.NewCache, cache.NewCache,
commands.NewCommander, commands.NewCommander,
logger.NewLogger,
), ),
fx.Invoke( fx.Invoke(
cli.Run, cli.Run,
@ -24,3 +38,33 @@ func main() {
) )
app.Run() app.Run()
} }
type AsyncInit func(func(ctx context.Context) error)
var ErrContextShutdown = errors.New("shutdown")
func Context(
lc fx.Lifecycle,
s fx.Shutdowner,
log *slog.Logger,
) (context.Context, AsyncInit) {
if log == nil {
log = slog.Default()
}
ctx, cn := context.WithCancelCause(context.Background())
lc.Append(fx.Hook{
OnStop: func(ctx context.Context) error {
cn(fmt.Errorf("%w: %w", context.Canceled, ErrContextShutdown))
return nil
},
})
return ctx, func(fn func(ctx context.Context) error) {
go func() {
err := fn(ctx)
if err != nil {
log.Error("Failed to run async hook", "err", err)
s.Shutdown()
}
}()
}
}

3
renovate.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": ["config:recommended"]
}

View File

@ -1,39 +0,0 @@
package app
import (
"log/slog"
"os"
"gfx.cafe/util/go/fxplus"
"git.asdf.cafe/abs3nt/gunner"
"github.com/lmittmann/tint"
"go.uber.org/fx"
"git.asdf.cafe/abs3nt/gspot/src/config"
"git.asdf.cafe/abs3nt/gspot/src/services"
)
var Services = fx.Options(
fx.NopLogger,
fx.Provide(
func() *slog.Logger {
return slog.New(tint.NewHandler(os.Stdout, &tint.Options{
Level: slog.LevelDebug.Level(),
TimeFormat: "[15:04:05.000]",
}))
},
services.NewSpotifyClient,
fxplus.Context,
),
)
var Config = fx.Options(
fx.Provide(
func() *config.Config {
c := &config.Config{}
gunner.LoadApp(c, "gspot")
return c
},
),
Services,
)

View File

@ -1,32 +1,32 @@
package cli package cli
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"strconv" "strconv"
"strings"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"github.com/zmb3/spotify/v2" "github.com/zmb3/spotify/v2"
"go.uber.org/fx" "go.uber.org/fx"
"git.asdf.cafe/abs3nt/gspot/src/components/commands" "git.asdf.cafe/abs3nt/gspot/src/components/commands"
"git.asdf.cafe/abs3nt/gspot/src/components/tui" "git.asdf.cafe/abs3nt/gspot/src/components/tui"
"git.asdf.cafe/abs3nt/gspot/src/components/tuitview"
) )
var Version = "dev" var Version = "dev"
func Run(c *commands.Commander, s fx.Shutdowner) { func Run(c *commands.Commander, s fx.Shutdowner) {
defer func() { app := &cli.Command{
err := s.Shutdown() Name: "gspot",
if err != nil { EnableShellCompletion: true,
c.Log.Error("SHUTDOWN", "error shutting down", err) Version: Version,
} Action: func(ctx context.Context, cmd *cli.Command) error {
}() if cmd.Args().Present() {
app := &cli.App{ return fmt.Errorf("unknown command: %s", strings.Join(cmd.Args().Slice(), " "))
Name: "gspot", }
EnableBashCompletion: true,
Version: Version,
Action: func(ctx *cli.Context) error {
return tui.StartTea(c, "main") return tui.StartTea(c, "main")
}, },
Commands: []*cli.Command{ Commands: []*cli.Command{
@ -34,7 +34,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "play", Name: "play",
Aliases: []string{"pl", "start", "s"}, Aliases: []string{"pl", "start", "s"},
Usage: "Plays spotify", Usage: "Plays spotify",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.Play() return c.Play()
}, },
Category: "Playback", Category: "Playback",
@ -43,10 +46,15 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "playurl", Name: "playurl",
Aliases: []string{"plu"}, Aliases: []string{"plu"},
Usage: "Plays a spotify url", Usage: "Plays a spotify url",
Args: true,
ArgsUsage: "url", ArgsUsage: "url",
Action: func(ctx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
return c.PlayUrl(ctx.Args().First()) if !cmd.Args().Present() {
return fmt.Errorf("no url provided")
}
if cmd.NArg() > 1 {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.PlayURL(cmd.Args().First())
}, },
Category: "Playback", Category: "Playback",
}, },
@ -54,7 +62,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "pause", Name: "pause",
Aliases: []string{"pa"}, Aliases: []string{"pa"},
Usage: "Pauses spotify", Usage: "Pauses spotify",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.Pause() return c.Pause()
}, },
Category: "Playback", Category: "Playback",
@ -63,7 +74,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "toggleplay", Name: "toggleplay",
Aliases: []string{"t"}, Aliases: []string{"t"},
Usage: "Toggles play/pause", Usage: "Toggles play/pause",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.TogglePlay() return c.TogglePlay()
}, },
Category: "Playback", Category: "Playback",
@ -72,7 +86,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "link", Name: "link",
Aliases: []string{"yy"}, Aliases: []string{"yy"},
Usage: "Prints the current song's spotify link", Usage: "Prints the current song's spotify link",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.PrintLink() return c.PrintLink()
}, },
Category: "Sharing", Category: "Sharing",
@ -81,7 +98,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "linkcontext", Name: "linkcontext",
Aliases: []string{"lc"}, Aliases: []string{"lc"},
Usage: "Prints the current album or playlist", Usage: "Prints the current album or playlist",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.PrintLinkContext() return c.PrintLinkContext()
}, },
Category: "Sharing", Category: "Sharing",
@ -90,7 +110,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "youtube-link", Name: "youtube-link",
Aliases: []string{"yl"}, Aliases: []string{"yl"},
Usage: "Prints the current song's youtube link", Usage: "Prints the current song's youtube link",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.PrintYoutubeLink() return c.PrintYoutubeLink()
}, },
Category: "Sharing", Category: "Sharing",
@ -99,11 +122,13 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "next", Name: "next",
Aliases: []string{"n", "skip"}, Aliases: []string{"n", "skip"},
Usage: "Skips to the next song", Usage: "Skips to the next song",
Args: true,
ArgsUsage: "amount", ArgsUsage: "amount",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cCtx.NArg() > 0 { if cmd.NArg() > 1 {
amt, err := strconv.Atoi(cCtx.Args().First()) return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
if cmd.NArg() > 0 {
amt, err := strconv.Atoi(cmd.Args().First())
if err != nil { if err != nil {
return err return err
} }
@ -117,7 +142,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "previous", Name: "previous",
Aliases: []string{"b", "prev", "back"}, Aliases: []string{"b", "prev", "back"},
Usage: "Skips to the previous song", Usage: "Skips to the previous song",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.Previous() return c.Previous()
}, },
Category: "Playback", Category: "Playback",
@ -126,7 +154,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "like", Name: "like",
Aliases: []string{"l"}, Aliases: []string{"l"},
Usage: "Likes the current song", Usage: "Likes the current song",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.Like() return c.Like()
}, },
Category: "Library Management", Category: "Library Management",
@ -135,7 +166,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "unlike", Name: "unlike",
Aliases: []string{"u"}, Aliases: []string{"u"},
Usage: "Unlikes the current song", Usage: "Unlikes the current song",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.UnLike() return c.UnLike()
}, },
Category: "Library Management", Category: "Library Management",
@ -152,8 +186,11 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Usage: "bypass cache", Usage: "bypass cache",
}, },
}, },
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
return c.NowPlaying(cCtx.Bool("force")) if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.NowPlaying(cmd.Bool("force"))
}, },
Category: "Info", Category: "Info",
}, },
@ -162,14 +199,16 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Aliases: []string{"v"}, Aliases: []string{"v"},
Usage: "Control the volume", Usage: "Control the volume",
Category: "Playback", Category: "Playback",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
{ {
Name: "up", Name: "up",
Usage: "Increase the volume", Usage: "Increase the volume",
Args: true,
ArgsUsage: "percent", ArgsUsage: "percent",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
amt, err := strconv.Atoi(cCtx.Args().First()) if cmd.NArg() > 1 {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
amt, err := strconv.Atoi(cmd.Args().First())
if err != nil { if err != nil {
return err return err
} }
@ -180,10 +219,12 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "down", Name: "down",
Aliases: []string{"dn"}, Aliases: []string{"dn"},
Usage: "Decrease the volume", Usage: "Decrease the volume",
Args: true,
ArgsUsage: "percent", ArgsUsage: "percent",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
amt, err := strconv.Atoi(cCtx.Args().First()) if cmd.NArg() > 1 {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
amt, err := strconv.Atoi(cmd.Args().First())
if err != nil { if err != nil {
return err return err
} }
@ -194,7 +235,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "mute", Name: "mute",
Aliases: []string{"m"}, Aliases: []string{"m"},
Usage: "Mute", Usage: "Mute",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.Mute() return c.Mute()
}, },
}, },
@ -202,7 +246,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "unmute", Name: "unmute",
Aliases: []string{"um"}, Aliases: []string{"um"},
Usage: "Unmute", Usage: "Unmute",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.UnMute() return c.UnMute()
}, },
}, },
@ -210,7 +257,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "togglemute", Name: "togglemute",
Aliases: []string{"tm"}, Aliases: []string{"tm"},
Usage: "Toggle mute", Usage: "Toggle mute",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.ToggleMute() return c.ToggleMute()
}, },
}, },
@ -220,15 +270,17 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "download_cover", Name: "download_cover",
Usage: "Downloads the cover of the current song", Usage: "Downloads the cover of the current song",
Aliases: []string{"dl"}, Aliases: []string{"dl"},
Args: true,
ArgsUsage: "path", ArgsUsage: "path",
BashComplete: func(cCtx *cli.Context) { ShellComplete: func(ctx context.Context, cmd *cli.Command) {
if cCtx.NArg() > 0 { if cmd.NArg() > 0 {
return return
} }
}, },
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
return c.DownloadCover(cCtx.Args().First()) if cmd.NArg() > 1 {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.DownloadCover(cmd.Args().First())
}, },
Category: "Info", Category: "Info",
}, },
@ -236,7 +288,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "radio", Name: "radio",
Usage: "Starts a radio from the current song", Usage: "Starts a radio from the current song",
Aliases: []string{"r"}, Aliases: []string{"r"},
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.Radio() return c.Radio()
}, },
Category: "Radio", Category: "Radio",
@ -245,7 +300,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "clearradio", Name: "clearradio",
Usage: "Clears the radio queue", Usage: "Clears the radio queue",
Aliases: []string{"cr"}, Aliases: []string{"cr"},
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.ClearRadio() return c.ClearRadio()
}, },
Category: "Radio", Category: "Radio",
@ -254,7 +312,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "refillradio", Name: "refillradio",
Usage: "Refills the radio queue with similar songs", Usage: "Refills the radio queue with similar songs",
Aliases: []string{"rr"}, Aliases: []string{"rr"},
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.RefillRadio() return c.RefillRadio()
}, },
Category: "Radio", Category: "Radio",
@ -262,7 +323,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
{ {
Name: "status", Name: "status",
Usage: "Prints the current status", Usage: "Prints the current status",
Action: func(ctx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.Status() return c.Status()
}, },
Category: "Info", Category: "Info",
@ -271,7 +335,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "devices", Name: "devices",
Usage: "Lists available devices", Usage: "Lists available devices",
Aliases: []string{"d"}, Aliases: []string{"d"},
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.ListDevices() return c.ListDevices()
}, },
Category: "Info", Category: "Info",
@ -279,20 +346,25 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
{ {
Name: "setdevice", Name: "setdevice",
Usage: "Set the active device", Usage: "Set the active device",
Args: true,
ArgsUsage: "<device_id>", ArgsUsage: "<device_id>",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cCtx.NArg() == 0 { if cmd.NArg() == 0 {
return fmt.Errorf("no device id provided") return fmt.Errorf("no device id provided")
} }
return c.SetDevice(spotify.ID(cCtx.Args().First())) if cmd.NArg() > 1 {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.SetDevice(spotify.ID(cmd.Args().First()))
}, },
Category: "Playback", Category: "Playback",
}, },
{ {
Name: "repeat", Name: "repeat",
Usage: "Toggle repeat mode", Usage: "Toggle repeat mode",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.Repeat() return c.Repeat()
}, },
Category: "Playback", Category: "Playback",
@ -300,7 +372,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
{ {
Name: "shuffle", Name: "shuffle",
Usage: "Toggle shuffle mode", Usage: "Toggle shuffle mode",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.Shuffle() return c.Shuffle()
}, },
Category: "Playback", Category: "Playback",
@ -308,28 +383,48 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
{ {
Name: "tui", Name: "tui",
Usage: "Starts the TUI", Usage: "Starts the TUI",
Action: func(ctx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return tui.StartTea(c, "main") return tui.StartTea(c, "main")
}, },
}, },
{
Name: "tview",
Usage: "Starts the TUI using tview (experimental)",
Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
// start tview tui
return tuitview.TuitView(c)
},
},
{ {
Name: "seek", Name: "seek",
Usage: "Seek to a position in the song", Usage: "Seek to a position in the song",
Aliases: []string{"sk"}, Aliases: []string{"sk"},
Category: "Playback", Category: "Playback",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
pos, err := strconv.Atoi(cCtx.Args().First()) if cmd.NArg() > 1 {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
pos, err := strconv.Atoi(cmd.Args().First())
if err != nil { if err != nil {
return err return err
} }
return c.SetPosition(pos) return c.SetPosition(pos)
}, },
Subcommands: []*cli.Command{ Commands: []*cli.Command{
{ {
Name: "forward", Name: "forward",
Aliases: []string{"f"}, Aliases: []string{"f"},
Usage: "Seek forward", Usage: "Seek forward",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.Seek(true) return c.Seek(true)
}, },
}, },
@ -337,7 +432,10 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Name: "backward", Name: "backward",
Aliases: []string{"b"}, Aliases: []string{"b"},
Usage: "Seek backward", Usage: "Seek backward",
Action: func(cCtx *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
return c.Seek(false) return c.Seek(false)
}, },
}, },
@ -345,8 +443,9 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
}, },
}, },
} }
if err := app.Run(os.Args); err != nil { if err := app.Run(c.Context, os.Args); err != nil {
c.Log.Error("COMMANDER", "run error", err) c.Log.Error("COMMANDER", "run error", err)
os.Exit(1) s.Shutdown(fx.ExitCode(1))
} }
s.Shutdown()
} }

View File

@ -26,7 +26,7 @@ func (c *Commander) activateDevice() (spotify.ID, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
err = c.Client.TransferPlayback(c.Context, device.ID, true) err = c.Client().TransferPlayback(c.Context, device.ID, true)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -5,7 +5,7 @@ import (
) )
func (c *Commander) AlbumTracks(album spotify.ID, page int) (*spotify.SimpleTrackPage, error) { func (c *Commander) AlbumTracks(album spotify.ID, page int) (*spotify.SimpleTrackPage, error) {
tracks, err := c.Client. tracks, err := c.Client().
GetAlbumTracks(c.Context, album, spotify.Limit(50), spotify.Offset((page-1)*50), spotify.Market(spotify.CountryUSA)) GetAlbumTracks(c.Context, album, spotify.Limit(50), spotify.Offset((page-1)*50), spotify.Market(spotify.CountryUSA))
if err != nil { if err != nil {
return nil, err return nil, err
@ -14,5 +14,5 @@ func (c *Commander) AlbumTracks(album spotify.ID, page int) (*spotify.SimpleTrac
} }
func (c *Commander) UserAlbums(page int) (*spotify.SavedAlbumPage, error) { func (c *Commander) UserAlbums(page int) (*spotify.SavedAlbumPage, error) {
return c.Client.CurrentUsersAlbums(c.Context, spotify.Limit(50), spotify.Offset((page-1)*50)) return c.Client().CurrentUsersAlbums(c.Context, spotify.Limit(50), spotify.Offset((page-1)*50))
} }

View File

@ -3,7 +3,7 @@ package commands
import "github.com/zmb3/spotify/v2" import "github.com/zmb3/spotify/v2"
func (c *Commander) ArtistAlbums(artist spotify.ID, page int) (*spotify.SimpleAlbumPage, error) { func (c *Commander) ArtistAlbums(artist spotify.ID, page int) (*spotify.SimpleAlbumPage, error) {
albums, err := c.Client. albums, err := c.Client().
GetArtistAlbums(c.Context, artist, []spotify.AlbumType{1, 2, 3, 4}, spotify.Market(spotify.CountryUSA), spotify.Limit(50), spotify.Offset((page-1)*50)) GetArtistAlbums(c.Context, artist, []spotify.AlbumType{1, 2, 3, 4}, spotify.Market(spotify.CountryUSA), spotify.Limit(50), spotify.Offset((page-1)*50))
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -3,12 +3,14 @@ package commands
import ( import (
"context" "context"
"log/slog" "log/slog"
"os" "sync"
"github.com/zmb3/spotify/v2" "github.com/zmb3/spotify/v2"
"go.uber.org/fx" "go.uber.org/fx"
"git.asdf.cafe/abs3nt/gspot/src/components/cache" "git.asdf.cafe/abs3nt/gspot/src/components/cache"
"git.asdf.cafe/abs3nt/gspot/src/config"
"git.asdf.cafe/abs3nt/gspot/src/services"
) )
type CommanderResult struct { type CommanderResult struct {
@ -21,33 +23,53 @@ type CommanderParams struct {
fx.In fx.In
Context context.Context Context context.Context
Client *spotify.Client
Log *slog.Logger Log *slog.Logger
Cache *cache.Cache Cache *cache.Cache
Config *config.Config
} }
type Commander struct { type Commander struct {
Context context.Context Context context.Context
Client *spotify.Client
User *spotify.PrivateUser User *spotify.PrivateUser
Log *slog.Logger Log *slog.Logger
Cache *cache.Cache Cache *cache.Cache
mu sync.RWMutex
cl *spotify.Client
conf *config.Config
} }
func NewCommander(p CommanderParams) CommanderResult { func NewCommander(p CommanderParams) CommanderResult {
currentUser, err := p.Client.CurrentUser(p.Context)
if err != nil {
slog.Error("COMMANDER", "error getting current user", err)
os.Exit(1)
}
c := &Commander{ c := &Commander{
Context: p.Context, Context: p.Context,
Client: p.Client,
User: currentUser,
Log: p.Log, Log: p.Log,
Cache: p.Cache, Cache: p.Cache,
conf: p.Config,
} }
return CommanderResult{ return CommanderResult{
Commander: c, Commander: c,
} }
} }
func (c *Commander) Client() *spotify.Client {
c.mu.Lock()
if c.cl == nil {
c.cl = c.connectClient()
}
c.mu.Unlock()
c.mu.RLock()
defer c.mu.RUnlock()
return c.cl
}
func (c *Commander) connectClient() *spotify.Client {
client, err := services.GetClient(c.conf)
if err != nil {
panic(err)
}
currentUser, err := client.CurrentUser(c.Context)
if err != nil {
panic(err)
}
c.User = currentUser
return client
}

View File

@ -10,7 +10,7 @@ import (
) )
func (c *Commander) ListDevices() error { func (c *Commander) ListDevices() error {
devices, err := c.Client.PlayerDevices(c.Context) devices, err := c.Client().PlayerDevices(c.Context)
if err != nil { if err != nil {
return err return err
} }
@ -27,11 +27,11 @@ func PrintDevices(devices []spotify.PlayerDevice) error {
} }
func (c *Commander) SetDevice(device spotify.ID) error { func (c *Commander) SetDevice(device spotify.ID) error {
err := c.Client.TransferPlayback(c.Context, device, true) err := c.Client().TransferPlayback(c.Context, device, true)
if err != nil { if err != nil {
return err return err
} }
devices, err := c.Client.PlayerDevices(c.Context) devices, err := c.Client().PlayerDevices(c.Context)
if err != nil { if err != nil {
return err return err
} }

View File

@ -10,7 +10,7 @@ func (c *Commander) DownloadCover(path string) error {
path = "cover.png" path = "cover.png"
} }
destinationPath := filepath.Clean(path) destinationPath := filepath.Clean(path)
state, err := c.Client.PlayerState(c.Context) state, err := c.Client().PlayerState(c.Context)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,9 +1,9 @@
package commands package commands
func (c *Commander) Like() error { func (c *Commander) Like() error {
playing, err := c.Client.PlayerCurrentlyPlaying(c.Context) playing, err := c.Client().PlayerCurrentlyPlaying(c.Context)
if err != nil { if err != nil {
return err return err
} }
return c.Client.AddTracksToLibrary(c.Context, playing.Item.ID) return c.Client().AddTracksToLibrary(c.Context, playing.Item.ID)
} }

View File

@ -3,7 +3,7 @@ package commands
import "fmt" import "fmt"
func (c *Commander) PrintLink() error { func (c *Commander) PrintLink() error {
state, err := c.Client.PlayerState(c.Context) state, err := c.Client().PlayerState(c.Context)
if err != nil { if err != nil {
return err return err
} }
@ -12,7 +12,7 @@ func (c *Commander) PrintLink() error {
} }
func (c *Commander) PrintLinkContext() error { func (c *Commander) PrintLinkContext() error {
state, err := c.Client.PlayerState(c.Context) state, err := c.Client().PlayerState(c.Context)
if err != nil { if err != nil {
return err return err
} }

View File

@ -9,7 +9,7 @@ import (
func (c *Commander) Next(amt int, inqueue bool) error { func (c *Commander) Next(amt int, inqueue bool) error {
if inqueue { if inqueue {
for i := 0; i < amt; i++ { for i := 0; i < amt; i++ {
err := c.Client.Next(c.Context) err := c.Client().Next(c.Context)
if err != nil { if err != nil {
return err return err
} }
@ -17,15 +17,15 @@ func (c *Commander) Next(amt int, inqueue bool) error {
return nil return nil
} }
if amt == 1 { if amt == 1 {
err := c.Client.Next(c.Context) err := c.Client().Next(c.Context)
if err != nil { if err != nil {
if isNoActiveError(err) { if isNoActiveError(err) {
deviceId, err := c.activateDevice() deviceID, err := c.activateDevice()
if err != nil { if err != nil {
return err return err
} }
err = c.Client.NextOpt(c.Context, &spotify.PlayOptions{ err = c.Client().NextOpt(c.Context, &spotify.PlayOptions{
DeviceID: &deviceId, DeviceID: &deviceID,
}) })
if err != nil { if err != nil {
return err return err
@ -37,7 +37,7 @@ func (c *Commander) Next(amt int, inqueue bool) error {
} }
// found := false // found := false
// playingIndex := 0 // playingIndex := 0
current, err := c.Client.PlayerCurrentlyPlaying(c.Context) current, err := c.Client().PlayerCurrentlyPlaying(c.Context)
if err != nil { if err != nil {
return err return err
} }
@ -48,7 +48,7 @@ func (c *Commander) Next(amt int, inqueue bool) error {
currentTrackIndex := 0 currentTrackIndex := 0
page := 1 page := 1
for !found { for !found {
playlist, err := c.Client. playlist, err := c.Client().
GetPlaylistItems( GetPlaylistItems(
c.Context, c.Context,
spotify.ID(strings.Split(string(current.PlaybackContext.URI), ":")[2]), spotify.ID(strings.Split(string(current.PlaybackContext.URI), ":")[2]),
@ -68,7 +68,7 @@ func (c *Commander) Next(amt int, inqueue bool) error {
page++ page++
} }
pos := currentTrackIndex + amt pos := currentTrackIndex + amt
return c.Client.PlayOpt(c.Context, &spotify.PlayOptions{ return c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &current.PlaybackContext.URI, PlaybackContext: &current.PlaybackContext.URI,
PlaybackOffset: &spotify.PlaybackOffset{ PlaybackOffset: &spotify.PlaybackOffset{
Position: &pos, Position: &pos,
@ -79,7 +79,7 @@ func (c *Commander) Next(amt int, inqueue bool) error {
currentTrackIndex := 0 currentTrackIndex := 0
page := 1 page := 1
for !found { for !found {
playlist, err := c.Client. playlist, err := c.Client().
GetAlbumTracks( GetAlbumTracks(
c.Context, c.Context,
spotify.ID(strings.Split(string(current.PlaybackContext.URI), ":")[2]), spotify.ID(strings.Split(string(current.PlaybackContext.URI), ":")[2]),
@ -99,7 +99,7 @@ func (c *Commander) Next(amt int, inqueue bool) error {
page++ page++
} }
pos := currentTrackIndex + amt pos := currentTrackIndex + amt
return c.Client.PlayOpt(c.Context, &spotify.PlayOptions{ return c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &current.PlaybackContext.URI, PlaybackContext: &current.PlaybackContext.URI,
PlaybackOffset: &spotify.PlaybackOffset{ PlaybackOffset: &spotify.PlaybackOffset{
Position: &pos, Position: &pos,
@ -107,7 +107,7 @@ func (c *Commander) Next(amt int, inqueue bool) error {
}) })
default: default:
for i := 0; i < amt; i++ { for i := 0; i < amt; i++ {
err := c.Client.Next(c.Context) err := c.Client().Next(c.Context)
if err != nil { if err != nil {
return err return err
} }

View File

@ -9,7 +9,7 @@ import (
func (c *Commander) NowPlaying(force bool) error { func (c *Commander) NowPlaying(force bool) error {
if force { if force {
current, err := c.Client.PlayerCurrentlyPlaying(c.Context) current, err := c.Client().PlayerCurrentlyPlaying(c.Context)
if err != nil { if err != nil {
return err return err
} }
@ -19,7 +19,7 @@ func (c *Commander) NowPlaying(force bool) error {
return err return err
} }
song, err := c.Cache.GetOrDo("now_playing", func() (string, error) { song, err := c.Cache.GetOrDo("now_playing", func() (string, error) {
current, err := c.Client.PlayerCurrentlyPlaying(c.Context) current, err := c.Client().PlayerCurrentlyPlaying(c.Context)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -1,5 +1,5 @@
package commands package commands
func (c *Commander) Pause() error { func (c *Commander) Pause() error {
return c.Client.Pause(c.Context) return c.Client().Pause(c.Context)
} }

View File

@ -1,6 +1,7 @@
package commands package commands
import ( import (
"fmt"
"net/url" "net/url"
"strings" "strings"
@ -8,14 +9,14 @@ import (
) )
func (c *Commander) Play() error { func (c *Commander) Play() error {
err := c.Client.Play(c.Context) err := c.Client().Play(c.Context)
if err != nil { if err != nil {
if isNoActiveError(err) { if isNoActiveError(err) {
deviceID, err := c.activateDevice() deviceID, err := c.activateDevice()
if err != nil { if err != nil {
return err return err
} }
err = c.Client.PlayOpt(c.Context, &spotify.PlayOptions{ err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
DeviceID: &deviceID, DeviceID: &deviceID,
}) })
if err != nil { if err != nil {
@ -29,6 +30,7 @@ func (c *Commander) Play() error {
} }
func (c *Commander) PlayLikedSongs(position int) error { func (c *Commander) PlayLikedSongs(position int) error {
c.Log.Debug("Playing liked songs")
err := c.ClearRadio() err := c.ClearRadio()
if err != nil { if err != nil {
return err return err
@ -37,28 +39,31 @@ func (c *Commander) PlayLikedSongs(position int) error {
if err != nil { if err != nil {
return err return err
} }
songs, err := c.Client.CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(position)) c.Log.Debug("got playlist", "id", playlist.ID)
songs, err := c.Client().CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(position))
if err != nil { if err != nil {
return err return err
} }
to_add := []spotify.ID{} toAdd := []spotify.ID{}
for _, song := range songs.Tracks { for _, song := range songs.Tracks {
to_add = append(to_add, song.ID) toAdd = append(toAdd, song.ID)
} }
_, err = c.Client.AddTracksToPlaylist(c.Context, playlist.ID, to_add...) _, err = c.Client().AddTracksToPlaylist(c.Context, playlist.ID, toAdd...)
if err != nil { if err != nil {
return err return err
} }
err = c.Client.PlayOpt(c.Context, &spotify.PlayOptions{ c.Log.Debug("added songs to playlist")
err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &playlist.URI, PlaybackContext: &playlist.URI,
}) })
if err != nil { if err != nil {
if isNoActiveError(err) { if isNoActiveError(err) {
c.Log.Debug("need to activate device")
deviceID, err := c.activateDevice() deviceID, err := c.activateDevice()
if err != nil { if err != nil {
return err return err
} }
err = c.Client.PlayOpt(c.Context, &spotify.PlayOptions{ err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &playlist.URI, PlaybackContext: &playlist.URI,
DeviceID: &deviceID, DeviceID: &deviceID,
}) })
@ -67,44 +72,50 @@ func (c *Commander) PlayLikedSongs(position int) error {
} }
} }
} }
c.Log.Debug("starting loop")
for page := 2; page <= 5; page++ { for page := 2; page <= 5; page++ {
songs, err := c.Client.CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset((50*(page-1))+position)) c.Log.Debug("doing loop", "page", page)
songs, err := c.Client().CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset((50*(page-1))+position))
if err != nil { if err != nil {
return err return err
} }
to_add := []spotify.ID{} toAdd := []spotify.ID{}
for _, song := range songs.Tracks { for _, song := range songs.Tracks {
to_add = append(to_add, song.ID) toAdd = append(toAdd, song.ID)
} }
_, err = c.Client.AddTracksToPlaylist(c.Context, playlist.ID, to_add...) _, err = c.Client().AddTracksToPlaylist(c.Context, playlist.ID, toAdd...)
if err != nil { if err != nil {
return err return err
} }
} }
c.Log.Debug("done")
return err return err
} }
func (c *Commander) PlayUrl(urlString string) error { func (c *Commander) PlayURL(urlString string) error {
url, err := url.Parse(urlString) url, err := url.Parse(urlString)
if err != nil { if err != nil {
return err return err
} }
track_id := strings.Split(url.Path, "/")[2] splittUrl := strings.Split(url.Path, "/")
err = c.Client.QueueSong(c.Context, spotify.ID(track_id)) if len(splittUrl) < 3 {
return fmt.Errorf("invalid url")
}
trackID := splittUrl[2]
err = c.Client().QueueSong(c.Context, spotify.ID(trackID))
if err != nil { if err != nil {
if isNoActiveError(err) { if isNoActiveError(err) {
deviceID, err := c.activateDevice() deviceID, err := c.activateDevice()
if err != nil { if err != nil {
return err return err
} }
err = c.Client.QueueSongOpt(c.Context, spotify.ID(track_id), &spotify.PlayOptions{ err = c.Client().QueueSongOpt(c.Context, spotify.ID(trackID), &spotify.PlayOptions{
DeviceID: &deviceID, DeviceID: &deviceID,
}) })
if err != nil { if err != nil {
return err return err
} }
err = c.Client.NextOpt(c.Context, &spotify.PlayOptions{ err = c.Client().NextOpt(c.Context, &spotify.PlayOptions{
DeviceID: &deviceID, DeviceID: &deviceID,
}) })
if err != nil { if err != nil {
@ -115,7 +126,7 @@ func (c *Commander) PlayUrl(urlString string) error {
return err return err
} }
} }
err = c.Client.Next(c.Context) err = c.Client().Next(c.Context)
if err != nil { if err != nil {
return err return err
} }
@ -123,7 +134,7 @@ func (c *Commander) PlayUrl(urlString string) error {
} }
func (c *Commander) PlaySongInPlaylist(context *spotify.URI, offset *int) error { func (c *Commander) PlaySongInPlaylist(context *spotify.URI, offset *int) error {
e := c.Client.PlayOpt(c.Context, &spotify.PlayOptions{ e := c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackOffset: &spotify.PlaybackOffset{Position: offset}, PlaybackOffset: &spotify.PlaybackOffset{Position: offset},
PlaybackContext: context, PlaybackContext: context,
}) })
@ -133,7 +144,7 @@ func (c *Commander) PlaySongInPlaylist(context *spotify.URI, offset *int) error
if err != nil { if err != nil {
return err return err
} }
err = c.Client.PlayOpt(c.Context, &spotify.PlayOptions{ err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackOffset: &spotify.PlaybackOffset{Position: offset}, PlaybackOffset: &spotify.PlaybackOffset{Position: offset},
PlaybackContext: context, PlaybackContext: context,
DeviceID: &deviceID, DeviceID: &deviceID,
@ -144,7 +155,7 @@ func (c *Commander) PlaySongInPlaylist(context *spotify.URI, offset *int) error
if err != nil { if err != nil {
return err return err
} }
err = c.Client.PlayOpt(c.Context, &spotify.PlayOptions{ err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackOffset: &spotify.PlaybackOffset{Position: offset}, PlaybackOffset: &spotify.PlaybackOffset{Position: offset},
PlaybackContext: context, PlaybackContext: context,
DeviceID: &deviceID, DeviceID: &deviceID,
@ -154,7 +165,7 @@ func (c *Commander) PlaySongInPlaylist(context *spotify.URI, offset *int) error
} }
} }
} }
err = c.Client.Play(c.Context) err = c.Client().Play(c.Context)
if err != nil { if err != nil {
return err return err
} }

View File

@ -3,15 +3,15 @@ package commands
import "github.com/zmb3/spotify/v2" import "github.com/zmb3/spotify/v2"
func (c *Commander) Playlists(page int) (*spotify.SimplePlaylistPage, error) { func (c *Commander) Playlists(page int) (*spotify.SimplePlaylistPage, error) {
return c.Client.CurrentUsersPlaylists(c.Context, spotify.Limit(50), spotify.Offset((page-1)*50)) return c.Client().CurrentUsersPlaylists(c.Context, spotify.Limit(50), spotify.Offset((page-1)*50))
} }
func (c *Commander) PlaylistTracks(playlist spotify.ID, page int) (*spotify.PlaylistItemPage, error) { func (c *Commander) PlaylistTracks(playlist spotify.ID, page int) (*spotify.PlaylistItemPage, error) {
return c.Client.GetPlaylistItems(c.Context, playlist, spotify.Limit(50), spotify.Offset((page-1)*50)) return c.Client().GetPlaylistItems(c.Context, playlist, spotify.Limit(50), spotify.Offset((page-1)*50))
} }
func (c *Commander) DeleteTracksFromPlaylist(tracks []spotify.ID, playlist spotify.ID) error { func (c *Commander) DeleteTracksFromPlaylist(tracks []spotify.ID, playlist spotify.ID) error {
_, err := c.Client.RemoveTracksFromPlaylist(c.Context, playlist, tracks...) _, err := c.Client().RemoveTracksFromPlaylist(c.Context, playlist, tracks...)
if err != nil { if err != nil {
return err return err
} }
@ -19,5 +19,5 @@ func (c *Commander) DeleteTracksFromPlaylist(tracks []spotify.ID, playlist spoti
} }
func (c *Commander) TrackList(page int) (*spotify.SavedTrackPage, error) { func (c *Commander) TrackList(page int) (*spotify.SavedTrackPage, error) {
return c.Client.CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset((page-1)*50)) return c.Client().CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset((page-1)*50))
} }

View File

@ -5,15 +5,15 @@ import (
) )
func (c *Commander) Previous() error { func (c *Commander) Previous() error {
err := c.Client.Previous(c.Context) err := c.Client().Previous(c.Context)
if err != nil { if err != nil {
if isNoActiveError(err) { if isNoActiveError(err) {
deviceId, err := c.activateDevice() deviceID, err := c.activateDevice()
if err != nil { if err != nil {
return err return err
} }
err = c.Client.PreviousOpt(c.Context, &spotify.PlayOptions{ err = c.Client().PreviousOpt(c.Context, &spotify.PlayOptions{
DeviceID: &deviceId, DeviceID: &deviceID,
}) })
if err != nil { if err != nil {
return err return err

View File

@ -3,14 +3,14 @@ package commands
import "github.com/zmb3/spotify/v2" import "github.com/zmb3/spotify/v2"
func (c *Commander) QueueSong(id spotify.ID) error { func (c *Commander) QueueSong(id spotify.ID) error {
err := c.Client.QueueSong(c.Context, id) err := c.Client().QueueSong(c.Context, id)
if err != nil { if err != nil {
if isNoActiveError(err) { if isNoActiveError(err) {
deviceID, err := c.activateDevice() deviceID, err := c.activateDevice()
if err != nil { if err != nil {
return err return err
} }
err = c.Client.QueueSongOpt(c.Context, id, &spotify.PlayOptions{ err = c.Client().QueueSongOpt(c.Context, id, &spotify.PlayOptions{
DeviceID: &deviceID, DeviceID: &deviceID,
}) })
if err != nil { if err != nil {

View File

@ -6,55 +6,46 @@ import (
"errors" "errors"
"fmt" "fmt"
"math" "math"
"math/rand"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
"gfx.cafe/util/go/frand"
"github.com/zmb3/spotify/v2" "github.com/zmb3/spotify/v2"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
func (c *Commander) Radio() error { func (c *Commander) Radio() error {
current_song, err := c.Client.PlayerCurrentlyPlaying(c.Context) currentSong, err := c.Client().PlayerCurrentlyPlaying(c.Context)
if err != nil { if err != nil {
return err return err
} }
if current_song.Item != nil { if currentSong.Item != nil {
return c.RadioGivenSong(current_song.Item.SimpleTrack, current_song.Progress) return c.RadioGivenSong(currentSong.Item.SimpleTrack, currentSong.Progress)
} }
_, err = c.activateDevice() _, err = c.activateDevice()
if err != nil { if err != nil {
return err return err
} }
tracks, err := c.Client.CurrentUsersTracks(c.Context, spotify.Limit(10)) tracks, err := c.Client().CurrentUsersTracks(c.Context, spotify.Limit(10))
if err != nil { if err != nil {
return err return err
} }
return c.RadioGivenSong(tracks.Tracks[frand.Intn(len(tracks.Tracks))].SimpleTrack, 0) return c.RadioGivenSong(tracks.Tracks[rand.Intn(len(tracks.Tracks))].SimpleTrack, 0)
} }
func (c *Commander) RadioFromPlaylist(playlist spotify.SimplePlaylist) error { func (c *Commander) RadioFromPlaylist(playlist spotify.SimplePlaylist) error {
total := playlist.Tracks.Total playlistPage, err := c.Client().GetPlaylistItems(
if total == 0 {
return fmt.Errorf("this playlist is empty")
}
pages := int(math.Ceil(float64(total) / 50))
randomPage := 1
if pages > 1 {
randomPage = frand.Intn(pages-1) + 1
}
playlistPage, err := c.Client.GetPlaylistItems(
c.Context, c.Context,
playlist.ID, playlist.ID,
spotify.Limit(50), spotify.Limit(50),
spotify.Offset((randomPage-1)*50), spotify.Offset(0),
) )
if err != nil { if err != nil {
return err return err
} }
pageSongs := playlistPage.Items pageSongs := playlistPage.Items
frand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] }) rand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] })
seedCount := 5 seedCount := 5
if len(pageSongs) < seedCount { if len(pageSongs) < seedCount {
seedCount = len(pageSongs) seedCount = len(pageSongs)
@ -70,7 +61,7 @@ func (c *Commander) RadioFromPlaylist(playlist spotify.SimplePlaylist) error {
} }
func (c *Commander) RadioFromSavedTracks() error { func (c *Commander) RadioFromSavedTracks() error {
savedSongs, err := c.Client.CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(0)) savedSongs, err := c.Client().CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(0))
if err != nil { if err != nil {
return err return err
} }
@ -80,14 +71,14 @@ func (c *Commander) RadioFromSavedTracks() error {
pages := int(math.Ceil(float64(savedSongs.Total) / 50)) pages := int(math.Ceil(float64(savedSongs.Total) / 50))
randomPage := 1 randomPage := 1
if pages > 1 { if pages > 1 {
randomPage = frand.Intn(pages-1) + 1 randomPage = rand.Intn(pages-1) + 1
} }
trackPage, err := c.Client.CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(randomPage*50)) trackPage, err := c.Client().CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(randomPage*50))
if err != nil { if err != nil {
return err return err
} }
pageSongs := trackPage.Tracks pageSongs := trackPage.Tracks
frand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] }) rand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] })
seedCount := 4 seedCount := 4
seedIds := []spotify.ID{} seedIds := []spotify.ID{}
for idx, song := range pageSongs { for idx, song := range pageSongs {
@ -104,7 +95,7 @@ func (c *Commander) RadioGivenArtist(artist spotify.SimpleArtist) error {
seed := spotify.Seeds{ seed := spotify.Seeds{
Artists: []spotify.ID{artist.ID}, Artists: []spotify.ID{artist.ID},
} }
recomendations, err := c.Client.GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100)) recomendations, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
if err != nil { if err != nil {
return err return err
} }
@ -134,31 +125,32 @@ func (c *Commander) RadioGivenArtist(artist spotify.SimpleArtist) error {
queue = append(queue, rec) queue = append(queue, rec)
} }
} }
_, err = c.Client.AddTracksToPlaylist(c.Context, radioPlaylist.ID, queue...) _, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, queue...)
if err != nil { if err != nil {
return err return err
} }
err = c.Client.PlayOpt(c.Context, &spotify.PlayOptions{ err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI, PlaybackContext: &radioPlaylist.URI,
}) })
if err != nil { if err != nil {
return err return err
} }
err = c.Client.Repeat(c.Context, "context") err = c.Client().Repeat(c.Context, "context")
if err != nil { if err != nil {
return err return err
} }
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
id := frand.Intn(len(recomendationIds)-2) + 1 id := rand.Intn(len(recomendationIds)-2) + 1
seed := spotify.Seeds{ seed := spotify.Seeds{
Tracks: []spotify.ID{recomendationIds[id]}, Tracks: []spotify.ID{recomendationIds[id]},
} }
additional_recs, err := c.Client.GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100)) additionalRecs, err := c.Client().
GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
if err != nil { if err != nil {
return err return err
} }
additionalRecsIds := []spotify.ID{} additionalRecsIds := []spotify.ID{}
for _, song := range additional_recs.Tracks { for _, song := range additionalRecs.Tracks {
exists, err := c.SongExists(db, song.ID) exists, err := c.SongExists(db, song.ID)
if err != nil { if err != nil {
return err return err
@ -171,7 +163,7 @@ func (c *Commander) RadioGivenArtist(artist spotify.SimpleArtist) error {
additionalRecsIds = append(additionalRecsIds, song.ID) additionalRecsIds = append(additionalRecsIds, song.ID)
} }
} }
_, err = c.Client.AddTracksToPlaylist(c.Context, radioPlaylist.ID, additionalRecsIds...) _, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, additionalRecsIds...)
if err != nil { if err != nil {
return err return err
} }
@ -179,12 +171,12 @@ func (c *Commander) RadioGivenArtist(artist spotify.SimpleArtist) error {
return nil return nil
} }
func (c *Commander) RadioGivenSong(song spotify.SimpleTrack, pos int) error { func (c *Commander) RadioGivenSong(song spotify.SimpleTrack, pos spotify.Numeric) error {
start := time.Now().UnixMilli() start := time.Now().UnixMilli()
seed := spotify.Seeds{ seed := spotify.Seeds{
Tracks: []spotify.ID{song.ID}, Tracks: []spotify.ID{song.ID},
} }
recomendations, err := c.Client.GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(99)) recomendations, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(99))
if err != nil { if err != nil {
return err return err
} }
@ -218,15 +210,15 @@ func (c *Commander) RadioGivenSong(song spotify.SimpleTrack, pos int) error {
queue = append(queue, rec) queue = append(queue, rec)
} }
} }
_, err = c.Client.AddTracksToPlaylist(c.Context, radioPlaylist.ID, queue...) _, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, queue...)
if err != nil { if err != nil {
return err return err
} }
delay := time.Now().UnixMilli() - start delay := time.Now().UnixMilli() - start
if pos != 0 { if pos != 0 {
pos = pos + int(delay) pos = pos + spotify.Numeric(delay)
} }
err = c.Client.PlayOpt(c.Context, &spotify.PlayOptions{ err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI, PlaybackContext: &radioPlaylist.URI,
PositionMs: pos, PositionMs: pos,
}) })
@ -236,7 +228,7 @@ func (c *Commander) RadioGivenSong(song spotify.SimpleTrack, pos int) error {
if err != nil { if err != nil {
return err return err
} }
err = c.Client.PlayOpt(c.Context, &spotify.PlayOptions{ err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI, PlaybackContext: &radioPlaylist.URI,
DeviceID: &deviceID, DeviceID: &deviceID,
PositionMs: pos, PositionMs: pos,
@ -246,21 +238,22 @@ func (c *Commander) RadioGivenSong(song spotify.SimpleTrack, pos int) error {
} }
} }
} }
err = c.Client.Repeat(c.Context, "context") err = c.Client().Repeat(c.Context, "context")
if err != nil { if err != nil {
return err return err
} }
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
id := frand.Intn(len(recomendationIds)-2) + 1 id := rand.Intn(len(recomendationIds)-2) + 1
seed := spotify.Seeds{ seed := spotify.Seeds{
Tracks: []spotify.ID{recomendationIds[id]}, Tracks: []spotify.ID{recomendationIds[id]},
} }
additional_recs, err := c.Client.GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100)) additionalRecs, err := c.Client().
GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
if err != nil { if err != nil {
return err return err
} }
additionalRecsIds := []spotify.ID{} additionalRecsIds := []spotify.ID{}
for _, song := range additional_recs.Tracks { for _, song := range additionalRecs.Tracks {
exists, err := c.SongExists(db, song.ID) exists, err := c.SongExists(db, song.ID)
if err != nil { if err != nil {
return err return err
@ -273,7 +266,7 @@ func (c *Commander) RadioGivenSong(song spotify.SimpleTrack, pos int) error {
additionalRecsIds = append(additionalRecsIds, song.ID) additionalRecsIds = append(additionalRecsIds, song.ID)
} }
} }
_, err = c.Client.AddTracksToPlaylist(c.Context, radioPlaylist.ID, additionalRecsIds...) _, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, additionalRecsIds...)
if err != nil { if err != nil {
return err return err
} }
@ -286,14 +279,14 @@ func (c *Commander) ClearRadio() error {
if err != nil { if err != nil {
return err return err
} }
err = c.Client.UnfollowPlaylist(c.Context, radioPlaylist.ID) err = c.Client().UnfollowPlaylist(c.Context, radioPlaylist.ID)
if err != nil { if err != nil {
return err return err
} }
_, _ = db.Query("DROP TABLE IF EXISTS radio") _, _ = db.Query("DROP TABLE IF EXISTS radio")
configDir, _ := os.UserConfigDir() configDir, _ := os.UserConfigDir()
os.Remove(filepath.Join(configDir, "gspot/radio.json")) os.Remove(filepath.Join(configDir, "gspot/radio.json"))
_ = c.Client.Pause(c.Context) _ = c.Client().Pause(c.Context)
return nil return nil
} }
@ -321,7 +314,7 @@ func (c *Commander) GetRadioPlaylist(name string) (*spotify.FullPlaylist, *sql.D
func (c *Commander) CreateRadioPlaylist(name string) (*spotify.FullPlaylist, *sql.DB, error) { func (c *Commander) CreateRadioPlaylist(name string) (*spotify.FullPlaylist, *sql.DB, error) {
// private flag doesnt work // private flag doesnt work
configDir, _ := os.UserConfigDir() configDir, _ := os.UserConfigDir()
playlist, err := c.Client. playlist, err := c.Client().
CreatePlaylistForUser(c.Context, c.User.ID, name+" - autoradio", "Automanaged radio playlist", false, false) CreatePlaylistForUser(c.Context, c.User.ID, name+" - autoradio", "Automanaged radio playlist", false, false)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -344,9 +337,9 @@ func (c *Commander) CreateRadioPlaylist(name string) (*spotify.FullPlaylist, *sq
} }
func (c *Commander) SongExists(db *sql.DB, song spotify.ID) (bool, error) { func (c *Commander) SongExists(db *sql.DB, song spotify.ID) (bool, error) {
song_id := string(song) songID := string(song)
sqlStmt := `SELECT id FROM radio WHERE id = ?` sqlStmt := `SELECT id FROM radio WHERE id = ?`
err := db.QueryRow(sqlStmt, song_id).Scan(&song_id) err := db.QueryRow(sqlStmt, songID).Scan(&songID)
if err != nil { if err != nil {
if err != sql.ErrNoRows { if err != sql.ErrNoRows {
return false, err return false, err
@ -359,31 +352,32 @@ func (c *Commander) SongExists(db *sql.DB, song spotify.ID) (bool, error) {
} }
func (c *Commander) RefillRadio() error { func (c *Commander) RefillRadio() error {
status, err := c.Client.PlayerCurrentlyPlaying(c.Context) status, err := c.Client().PlayerCurrentlyPlaying(c.Context)
if err != nil { if err != nil {
return err return err
} }
paused := false
if !status.Playing { if !status.Playing {
return nil paused = true
} }
to_remove := []spotify.ID{} toRemove := []spotify.ID{}
radioPlaylist, db, err := c.GetRadioPlaylist("") radioPlaylist, db, err := c.GetRadioPlaylist("")
if err != nil { if err != nil {
return err return err
} }
if status.PlaybackContext.URI != radioPlaylist.URI { playlistItems, err := c.Client().GetPlaylistItems(c.Context, radioPlaylist.ID)
return nil
}
playlistItems, err := c.Client.GetPlaylistItems(c.Context, radioPlaylist.ID)
if err != nil { if err != nil {
return fmt.Errorf("orig playlist items: %w", err) return fmt.Errorf("orig playlist items: %w", err)
} }
if status.PlaybackContext.URI != radioPlaylist.URI || paused {
return c.RadioFromPlaylist(radioPlaylist.SimplePlaylist)
}
page := 0 page := 0
for { for {
tracks, err := c.Client.GetPlaylistItems(c.Context, radioPlaylist.ID, spotify.Limit(50), spotify.Offset(page*50)) tracks, err := c.Client().GetPlaylistItems(c.Context, radioPlaylist.ID, spotify.Limit(50), spotify.Offset(page*50))
if err != nil { if err != nil {
return fmt.Errorf("tracks: %w", err) return fmt.Errorf("tracks: %w", err)
} }
@ -394,15 +388,15 @@ func (c *Commander) RefillRadio() error {
if track.Track.Track.ID == status.Item.ID { if track.Track.Track.ID == status.Item.ID {
break break
} }
to_remove = append(to_remove, track.Track.Track.ID) toRemove = append(toRemove, track.Track.Track.ID)
} }
page++ page++
} }
if len(to_remove) > 0 { if len(toRemove) > 0 {
var trackGroups []spotify.ID var trackGroups []spotify.ID
for idx, item := range to_remove { for idx, item := range toRemove {
if idx%100 == 0 { if idx%100 == 0 && idx != 0 {
_, err = c.Client.RemoveTracksFromPlaylist(c.Context, radioPlaylist.ID, trackGroups...) _, err = c.Client().RemoveTracksFromPlaylist(c.Context, radioPlaylist.ID, trackGroups...)
trackGroups = []spotify.ID{} trackGroups = []spotify.ID{}
} }
trackGroups = append(trackGroups, item) trackGroups = append(trackGroups, item)
@ -410,14 +404,14 @@ func (c *Commander) RefillRadio() error {
return fmt.Errorf("error clearing playlist: %w", err) return fmt.Errorf("error clearing playlist: %w", err)
} }
} }
_, err := c.Client.RemoveTracksFromPlaylist(c.Context, radioPlaylist.ID, trackGroups...) _, err := c.Client().RemoveTracksFromPlaylist(c.Context, radioPlaylist.ID, trackGroups...)
if err != nil { if err != nil {
return err return err
} }
} }
to_add := 500 - (playlistItems.Total - len(to_remove)) toAdd := 500 - (int(playlistItems.Total) - len(toRemove))
playlistItems, err = c.Client.GetPlaylistItems(c.Context, radioPlaylist.ID) playlistItems, err = c.Client().GetPlaylistItems(c.Context, radioPlaylist.ID)
if err != nil { if err != nil {
return fmt.Errorf("playlist items: %w", err) return fmt.Errorf("playlist items: %w", err)
} }
@ -425,15 +419,15 @@ func (c *Commander) RefillRadio() error {
pages := int(math.Ceil(float64(total) / 50)) pages := int(math.Ceil(float64(total) / 50))
randomPage := 1 randomPage := 1
if pages > 1 { if pages > 1 {
randomPage = frand.Intn(pages-1) + 1 randomPage = rand.Intn(pages-1) + 1
} }
playlistPage, err := c.Client. playlistPage, err := c.Client().
GetPlaylistItems(c.Context, radioPlaylist.ID, spotify.Limit(50), spotify.Offset((randomPage-1)*50)) GetPlaylistItems(c.Context, radioPlaylist.ID, spotify.Limit(50), spotify.Offset((randomPage-1)*50))
if err != nil { if err != nil {
return fmt.Errorf("playlist page: %w", err) return fmt.Errorf("playlist page: %w", err)
} }
pageSongs := playlistPage.Items pageSongs := playlistPage.Items
frand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] }) rand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] })
seedCount := 5 seedCount := 5
if len(pageSongs) < seedCount { if len(pageSongs) < seedCount {
seedCount = len(pageSongs) seedCount = len(pageSongs)
@ -448,7 +442,7 @@ func (c *Commander) RefillRadio() error {
seed := spotify.Seeds{ seed := spotify.Seeds{
Tracks: seedIds, Tracks: seedIds,
} }
recomendations, err := c.Client.GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(95)) recomendations, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(95))
if err != nil { if err != nil {
return err return err
} }
@ -464,7 +458,7 @@ func (c *Commander) RefillRadio() error {
} }
queue := []spotify.ID{} queue := []spotify.ID{}
for idx, rec := range recomendationIds { for idx, rec := range recomendationIds {
if idx > to_add { if idx > toAdd {
break break
} }
_, err = db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", rec.String())) _, err = db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", rec.String()))
@ -473,40 +467,41 @@ func (c *Commander) RefillRadio() error {
} }
queue = append(queue, rec) queue = append(queue, rec)
} }
to_add -= len(queue) toAdd -= len(queue)
_, err = c.Client.AddTracksToPlaylist(c.Context, radioPlaylist.ID, queue...) _, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, queue...)
if err != nil { if err != nil {
return fmt.Errorf("add tracks: %w", err) return fmt.Errorf("add tracks: %w", err)
} }
err = c.Client.Repeat(c.Context, "context") err = c.Client().Repeat(c.Context, "context")
if err != nil { if err != nil {
return fmt.Errorf("repeat: %w", err) return fmt.Errorf("repeat: %w", err)
} }
for to_add > 0 { for toAdd > 0 {
id := frand.Intn(len(recomendationIds)-2) + 1 id := rand.Intn(len(recomendationIds)-2) + 1
seed := spotify.Seeds{ seed := spotify.Seeds{
Tracks: []spotify.ID{recomendationIds[id]}, Tracks: []spotify.ID{recomendationIds[id]},
} }
additional_recs, err := c.Client.GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100)) additionalRecs, err := c.Client().
GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
if err != nil { if err != nil {
return fmt.Errorf("get recs: %w", err) return fmt.Errorf("get recs: %w", err)
} }
additionalRecsIds := []spotify.ID{} additionalRecsIds := []spotify.ID{}
for idx, song := range additional_recs.Tracks { for idx, song := range additionalRecs.Tracks {
exists, err := c.SongExists(db, song.ID) exists, err := c.SongExists(db, song.ID)
if err != nil { if err != nil {
return fmt.Errorf("check song existence: %w", err) return fmt.Errorf("check song existence: %w", err)
} }
if !exists { if !exists {
if idx > to_add { if idx > toAdd {
break break
} }
additionalRecsIds = append(additionalRecsIds, song.ID) additionalRecsIds = append(additionalRecsIds, song.ID)
queue = append(queue, song.ID) queue = append(queue, song.ID)
} }
} }
to_add -= len(queue) toAdd -= len(queue)
_, err = c.Client.AddTracksToPlaylist(c.Context, radioPlaylist.ID, additionalRecsIds...) _, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, additionalRecsIds...)
if err != nil { if err != nil {
return fmt.Errorf("add tracks to playlist: %w", err) return fmt.Errorf("add tracks to playlist: %w", err)
} }
@ -526,14 +521,14 @@ func (c *Commander) RadioFromAlbum(album spotify.SimpleAlbum) error {
pages := int(math.Ceil(float64(total) / 50)) pages := int(math.Ceil(float64(total) / 50))
randomPage := 1 randomPage := 1
if pages > 1 { if pages > 1 {
randomPage = frand.Intn(pages-1) + 1 randomPage = rand.Intn(pages-1) + 1
} }
albumTrackPage, err := c.AlbumTracks(album.ID, randomPage) albumTrackPage, err := c.AlbumTracks(album.ID, randomPage)
if err != nil { if err != nil {
return err return err
} }
pageSongs := albumTrackPage.Tracks pageSongs := albumTrackPage.Tracks
frand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] }) rand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] })
seedCount := 5 seedCount := 5
if len(pageSongs) < seedCount { if len(pageSongs) < seedCount {
seedCount = len(pageSongs) seedCount = len(pageSongs)
@ -548,11 +543,11 @@ func (c *Commander) RadioFromAlbum(album spotify.SimpleAlbum) error {
return c.RadioGivenList(seedIds[:seedCount], album.Name) return c.RadioGivenList(seedIds[:seedCount], album.Name)
} }
func (c *Commander) RadioGivenList(song_ids []spotify.ID, name string) error { func (c *Commander) RadioGivenList(songs []spotify.ID, name string) error {
seed := spotify.Seeds{ seed := spotify.Seeds{
Tracks: song_ids, Tracks: songs,
} }
recomendations, err := c.Client.GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(99)) recomendations, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(99))
if err != nil { if err != nil {
return err return err
} }
@ -568,7 +563,7 @@ func (c *Commander) RadioGivenList(song_ids []spotify.ID, name string) error {
if err != nil { if err != nil {
return err return err
} }
queue := []spotify.ID{song_ids[0]} queue := []spotify.ID{songs[0]}
for _, rec := range recomendationIds { for _, rec := range recomendationIds {
exists, err := c.SongExists(db, rec) exists, err := c.SongExists(db, rec)
if err != nil { if err != nil {
@ -582,22 +577,22 @@ func (c *Commander) RadioGivenList(song_ids []spotify.ID, name string) error {
queue = append(queue, rec) queue = append(queue, rec)
} }
} }
_, err = c.Client.AddTracksToPlaylist(c.Context, radioPlaylist.ID, queue...) _, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, queue...)
if err != nil { if err != nil {
return err return err
} }
err = c.Client.PlayOpt(c.Context, &spotify.PlayOptions{ err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI, PlaybackContext: &radioPlaylist.URI,
}) })
if err != nil { if err != nil {
if isNoActiveError(err) { if isNoActiveError(err) {
deviceId, err := c.activateDevice() deviceID, err := c.activateDevice()
if err != nil { if err != nil {
return err return err
} }
err = c.Client.PlayOpt(c.Context, &spotify.PlayOptions{ err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI, PlaybackContext: &radioPlaylist.URI,
DeviceID: &deviceId, DeviceID: &deviceID,
}) })
if err != nil { if err != nil {
return err return err
@ -605,16 +600,16 @@ func (c *Commander) RadioGivenList(song_ids []spotify.ID, name string) error {
} }
} }
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
id := frand.Intn(len(recomendationIds)-2) + 1 id := rand.Intn(len(recomendationIds)-2) + 1
seed := spotify.Seeds{ seed := spotify.Seeds{
Tracks: []spotify.ID{recomendationIds[id]}, Tracks: []spotify.ID{recomendationIds[id]},
} }
additional_recs, err := c.Client.GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100)) additionalRecs, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
if err != nil { if err != nil {
return err return err
} }
additionalRecsIds := []spotify.ID{} additionalRecsIds := []spotify.ID{}
for _, song := range additional_recs.Tracks { for _, song := range additionalRecs.Tracks {
exists, err := c.SongExists(db, song.ID) exists, err := c.SongExists(db, song.ID)
if err != nil { if err != nil {
return err return err
@ -627,7 +622,7 @@ func (c *Commander) RadioGivenList(song_ids []spotify.ID, name string) error {
additionalRecsIds = append(additionalRecsIds, song.ID) additionalRecsIds = append(additionalRecsIds, song.ID)
} }
} }
_, err = c.Client.AddTracksToPlaylist(c.Context, radioPlaylist.ID, additionalRecsIds...) _, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, additionalRecsIds...)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,7 +1,7 @@
package commands package commands
func (c *Commander) Repeat() error { func (c *Commander) Repeat() error {
state, err := c.Client.PlayerState(c.Context) state, err := c.Client().PlayerState(c.Context)
if err != nil { if err != nil {
return err return err
} }
@ -10,7 +10,7 @@ func (c *Commander) Repeat() error {
newState = "context" newState = "context"
} }
// spotifyd only supports binary value for repeat, context or off, change when/if spotifyd is better // spotifyd only supports binary value for repeat, context or off, change when/if spotifyd is better
err = c.Client.Repeat(c.Context, newState) err = c.Client().Repeat(c.Context, newState)
if err != nil { if err != nil {
return err return err
} }

View File

@ -3,7 +3,7 @@ package commands
import "github.com/zmb3/spotify/v2" import "github.com/zmb3/spotify/v2"
func (c *Commander) Search(search string, page int) (*spotify.SearchResult, error) { func (c *Commander) Search(search string, page int) (*spotify.SearchResult, error) {
result, err := c.Client. result, err := c.Client().
Search(c.Context, search, spotify.SearchTypeAlbum|spotify.SearchTypeArtist|spotify.SearchTypeTrack|spotify.SearchTypePlaylist, spotify.Limit(50), spotify.Offset((page-1)*50)) Search(c.Context, search, spotify.SearchTypeAlbum|spotify.SearchTypeArtist|spotify.SearchTypeTrack|spotify.SearchTypePlaylist, spotify.Limit(50), spotify.Offset((page-1)*50))
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -1,7 +1,7 @@
package commands package commands
func (c *Commander) Seek(fwd bool) error { func (c *Commander) Seek(fwd bool) error {
current, err := c.Client.PlayerCurrentlyPlaying(c.Context) current, err := c.Client().PlayerCurrentlyPlaying(c.Context)
if err != nil { if err != nil {
return err return err
} }
@ -9,7 +9,7 @@ func (c *Commander) Seek(fwd bool) error {
if !fwd { if !fwd {
newPos = current.Progress - 5000 newPos = current.Progress - 5000
} }
err = c.Client.Seek(c.Context, newPos) err = c.Client().Seek(c.Context, int(newPos))
if err != nil { if err != nil {
return err return err
} }
@ -17,7 +17,7 @@ func (c *Commander) Seek(fwd bool) error {
} }
func (c *Commander) SetPosition(pos int) error { func (c *Commander) SetPosition(pos int) error {
err := c.Client.Seek(c.Context, pos) err := c.Client().Seek(c.Context, pos)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,11 +1,11 @@
package commands package commands
func (c *Commander) Shuffle() error { func (c *Commander) Shuffle() error {
state, err := c.Client.PlayerState(c.Context) state, err := c.Client().PlayerState(c.Context)
if err != nil { if err != nil {
return err return err
} }
err = c.Client.Shuffle(c.Context, !state.ShuffleState) err = c.Client().Shuffle(c.Context, !state.ShuffleState)
if err != nil { if err != nil {
return err return err
} }

View File

@ -10,7 +10,7 @@ import (
func (c *Commander) Status() error { func (c *Commander) Status() error {
state, err := c.Cache.GetOrDo("state", func() (string, error) { state, err := c.Cache.GetOrDo("state", func() (string, error) {
state, err := c.Client.PlayerState(c.Context) state, err := c.Client().PlayerState(c.Context)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -1,12 +1,12 @@
package commands package commands
func (c *Commander) TogglePlay() error { func (c *Commander) TogglePlay() error {
state, err := c.Client.PlayerState(c.Context) state, err := c.Client().PlayerState(c.Context)
if err != nil { if err != nil {
return err return c.Play()
} }
if state.Playing { if state.Playing {
return c.Client.Pause(c.Context) return c.Pause()
} }
return c.Client.Play(c.Context) return c.Play()
} }

View File

@ -1,9 +1,9 @@
package commands package commands
func (c *Commander) UnLike() error { func (c *Commander) UnLike() error {
playing, err := c.Client.PlayerCurrentlyPlaying(c.Context) playing, err := c.Client().PlayerCurrentlyPlaying(c.Context)
if err != nil { if err != nil {
return err return err
} }
return c.Client.RemoveTracksFromLibrary(c.Context, playing.Item.ID) return c.Client().RemoveTracksFromLibrary(c.Context, playing.Item.ID)
} }

View File

@ -1,18 +1,18 @@
package commands package commands
func (c *Commander) ChangeVolume(amount int) error { func (c *Commander) ChangeVolume(amount int) error {
state, err := c.Client.PlayerState(c.Context) state, err := c.Client().PlayerState(c.Context)
if err != nil { if err != nil {
return err return err
} }
newVolume := state.Device.Volume + amount newVolume := int(state.Device.Volume) + amount
if newVolume > 100 { if newVolume > 100 {
newVolume = 100 newVolume = 100
} }
if newVolume < 0 { if newVolume < 0 {
newVolume = 0 newVolume = 0
} }
return c.Client.Volume(c.Context, newVolume) return c.Client().Volume(c.Context, newVolume)
} }
func (c *Commander) Mute() error { func (c *Commander) Mute() error {
@ -24,7 +24,7 @@ func (c *Commander) UnMute() error {
} }
func (c *Commander) ToggleMute() error { func (c *Commander) ToggleMute() error {
state, err := c.Client.PlayerState(c.Context) state, err := c.Client().PlayerState(c.Context)
if err != nil { if err != nil {
return err return err
} }

View File

@ -7,7 +7,7 @@ import (
) )
func (c *Commander) PrintYoutubeLink() error { func (c *Commander) PrintYoutubeLink() error {
state, err := c.Client.PlayerState(c.Context) state, err := c.Client().PlayerState(c.Context)
if err != nil { if err != nil {
return err return err
} }

View File

@ -0,0 +1,67 @@
package logger
import (
"log/slog"
"os"
"path/filepath"
"strings"
"github.com/lmittmann/tint"
"go.uber.org/fx"
"git.asdf.cafe/abs3nt/gspot/src/config"
)
type LoggerResult struct {
fx.Out
Logger *slog.Logger
}
type LoggerParams struct {
fx.In
Config *config.Config
}
func NewLogger(p LoggerParams) LoggerResult {
lvl := slog.LevelInfo
configLevel := strings.ToUpper(p.Config.LogLevel)
switch configLevel {
case "INFO":
lvl = slog.LevelInfo
case "WARN":
lvl = slog.LevelWarn
case "ERROR":
lvl = slog.LevelError
case "DEBUG":
lvl = slog.LevelDebug
}
if strings.ToUpper(p.Config.LogOutput) == "FILE" {
fp := ""
p, err := os.UserConfigDir()
if err != nil {
p, err := os.UserHomeDir()
if err != nil {
os.Exit(1)
}
fp = filepath.Join(p, ".config", "gspot", "gspot.log")
} else {
fp = filepath.Join(p, "gspot", "gspot.log")
}
f, err := os.Create(fp)
if err != nil {
os.Exit(1)
}
return LoggerResult{
Logger: slog.New(slog.NewJSONHandler(f, &slog.HandlerOptions{
Level: lvl.Level(),
})),
}
}
return LoggerResult{
Logger: slog.New(tint.NewHandler(os.Stdout, &tint.Options{
Level: lvl.Level(),
TimeFormat: "[15:04:05.000]",
})),
}
}

View File

@ -32,10 +32,10 @@ func (m *mainModel) LoadMoreItems() {
for _, item := range items { for _, item := range items {
m.list.InsertItem(len(m.list.Items())+1, item) m.list.InsertItem(len(m.list.Items())+1, item)
} }
main_updates <- m mainUpdates <- m
return return
case "artists": case "artists":
artists, err := m.commands.Client.CurrentUsersFollowedArtists( artists, err := m.commands.Client().CurrentUsersFollowedArtists(
m.commands.Context, m.commands.Context,
spotify.Limit(50), spotify.Limit(50),
spotify.Offset((page)*50), spotify.Offset((page)*50),
@ -60,7 +60,7 @@ func (m *mainModel) LoadMoreItems() {
for _, item := range items { for _, item := range items {
m.list.InsertItem(len(m.list.Items())+1, item) m.list.InsertItem(len(m.list.Items())+1, item)
} }
main_updates <- m mainUpdates <- m
return return
case "album": case "album":
tracks, err := m.commands.AlbumTracks(m.album.ID, (page + 1)) tracks, err := m.commands.AlbumTracks(m.album.ID, (page + 1))
@ -80,7 +80,7 @@ func (m *mainModel) LoadMoreItems() {
for _, item := range items { for _, item := range items {
m.list.InsertItem(len(m.list.Items())+1, item) m.list.InsertItem(len(m.list.Items())+1, item)
} }
main_updates <- m mainUpdates <- m
return return
case "albums": case "albums":
albums, err := m.commands.UserAlbums(page + 1) albums, err := m.commands.UserAlbums(page + 1)
@ -99,7 +99,7 @@ func (m *mainModel) LoadMoreItems() {
for _, item := range items { for _, item := range items {
m.list.InsertItem(len(m.list.Items())+1, item) m.list.InsertItem(len(m.list.Items())+1, item)
} }
main_updates <- m mainUpdates <- m
return return
case "main": case "main":
playlists, err := m.commands.Playlists(page + 1) playlists, err := m.commands.Playlists(page + 1)
@ -117,7 +117,7 @@ func (m *mainModel) LoadMoreItems() {
for _, item := range items { for _, item := range items {
m.list.InsertItem(len(m.list.Items())+1, item) m.list.InsertItem(len(m.list.Items())+1, item)
} }
main_updates <- m mainUpdates <- m
return return
case "playlist": case "playlist":
playlistItems, err := m.commands.PlaylistTracks(m.playlist.ID, (page + 1)) playlistItems, err := m.commands.PlaylistTracks(m.playlist.ID, (page + 1))
@ -139,7 +139,7 @@ func (m *mainModel) LoadMoreItems() {
for _, item := range items { for _, item := range items {
m.list.InsertItem(len(m.list.Items())+1, item) m.list.InsertItem(len(m.list.Items())+1, item)
} }
main_updates <- m mainUpdates <- m
return return
case "tracks": case "tracks":
tracks, err := m.commands.TrackList(page + 1) tracks, err := m.commands.TrackList(page + 1)
@ -159,7 +159,7 @@ func (m *mainModel) LoadMoreItems() {
for _, item := range items { for _, item := range items {
m.list.InsertItem(len(m.list.Items())+1, item) m.list.InsertItem(len(m.list.Items())+1, item)
} }
main_updates <- m mainUpdates <- m
return return
} }
} }

View File

@ -22,7 +22,7 @@ var (
DocStyle = lipgloss.NewStyle().Margin(0, 2).Border(lipgloss.DoubleBorder(), true, true, true, true) DocStyle = lipgloss.NewStyle().Margin(0, 2).Border(lipgloss.DoubleBorder(), true, true, true, true)
currentlyPlaying *spotify.CurrentlyPlaying currentlyPlaying *spotify.CurrentlyPlaying
playbackContext string playbackContext string
main_updates chan *mainModel mainUpdates chan *mainModel
page = 1 page = 1
loading = false loading = false
showingMessage = false showingMessage = false
@ -190,32 +190,32 @@ func (m *mainModel) GoBack() (tea.Cmd, error) {
return tea.Quit, nil return tea.Quit, nil
case Albums, Artists, Tracks, Playlist, Devices, Search, Queue: case Albums, Artists, Tracks, Playlist, Devices, Search, Queue:
m.mode = Main m.mode = Main
new_items, err := MainView(m.commands) newItems, err := MainView(m.commands)
if err != nil { if err != nil {
return nil, err return nil, err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
case Album: case Album:
m.mode = Albums m.mode = Albums
new_items, err := AlbumsView(m.commands) newItems, err := AlbumsView(m.commands)
if err != nil { if err != nil {
return nil, err return nil, err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
case Artist: case Artist:
m.mode = Artists m.mode = Artists
new_items, err := ArtistsView(m.commands) newItems, err := ArtistsView(m.commands)
if err != nil { if err != nil {
return nil, err return nil, err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
case ArtistAlbum: case ArtistAlbum:
m.mode = Artist m.mode = Artist
new_items, err := ArtistAlbumsView(m.artist.ID, m.commands) newItems, err := ArtistAlbumsView(m.artist.ID, m.commands)
if err != nil { if err != nil {
return nil, err return nil, err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
case SearchArtists, SearchTracks, SearchAlbums, SearchPlaylists: case SearchArtists, SearchTracks, SearchAlbums, SearchPlaylists:
m.mode = Search m.mode = Search
items, result, err := SearchView(m.commands, m.search) items, result, err := SearchView(m.commands, m.search)
@ -226,39 +226,39 @@ func (m *mainModel) GoBack() (tea.Cmd, error) {
m.list.SetItems(items) m.list.SetItems(items)
case SearchArtist: case SearchArtist:
m.mode = SearchArtists m.mode = SearchArtists
new_items, err := SearchArtistsView(m.commands, m.searchResults.Artists) newItems, err := SearchArtistsView(m.commands, m.searchResults.Artists)
if err != nil { if err != nil {
return nil, err return nil, err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
case SearchArtistAlbum: case SearchArtistAlbum:
m.mode = SearchArtist m.mode = SearchArtist
new_items, err := ArtistAlbumsView(m.artist.ID, m.commands) newItems, err := ArtistAlbumsView(m.artist.ID, m.commands)
if err != nil { if err != nil {
return nil, err return nil, err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
case SearchAlbum: case SearchAlbum:
m.mode = SearchAlbums m.mode = SearchAlbums
new_items, err := SearchAlbumsView(m.commands, m.searchResults.Albums) newItems, err := SearchAlbumsView(m.commands, m.searchResults.Albums)
if err != nil { if err != nil {
return nil, err return nil, err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
case SearchPlaylist: case SearchPlaylist:
m.mode = SearchPlaylists m.mode = SearchPlaylists
new_items, err := SearchPlaylistsView(m.commands, m.searchResults.Playlists) newItems, err := SearchPlaylistsView(m.commands, m.searchResults.Playlists)
if err != nil { if err != nil {
return nil, err return nil, err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
default: default:
page = 0 page = 0
} }
return nil, nil return nil, nil
} }
type SpotifyUrl struct { type SpotifyURL struct {
ExternalURLs map[string]string ExternalURLs map[string]string
} }
@ -267,34 +267,34 @@ func (m *mainModel) CopyToClipboard() error {
switch converted := item.(type) { switch converted := item.(type) {
case spotify.SimplePlaylist: case spotify.SimplePlaylist:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second) go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"]) return clipboard.WriteAll(converted.ExternalURLs["spotify"])
case *spotify.FullPlaylist: case *spotify.FullPlaylist:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second) go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"]) return clipboard.WriteAll(converted.ExternalURLs["spotify"])
case spotify.SimpleAlbum: case spotify.SimpleAlbum:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second) go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"]) return clipboard.WriteAll(converted.ExternalURLs["spotify"])
case *spotify.FullAlbum: case *spotify.FullAlbum:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second) go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"]) return clipboard.WriteAll(converted.ExternalURLs["spotify"])
case spotify.SimpleArtist: case spotify.SimpleArtist:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second) go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"]) return clipboard.WriteAll(converted.ExternalURLs["spotify"])
case *spotify.FullArtist: case *spotify.FullArtist:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second) go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"]) return clipboard.WriteAll(converted.ExternalURLs["spotify"])
case spotify.SimpleTrack: case spotify.SimpleTrack:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second) go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"]) return clipboard.WriteAll(converted.ExternalURLs["spotify"])
case spotify.PlaylistTrack: case spotify.PlaylistTrack:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second) go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.Track.ExternalURLs["spotify"]) return clipboard.WriteAll(converted.Track.ExternalURLs["spotify"])
case spotify.SavedTrack: case spotify.SavedTrack:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second) go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"]) return clipboard.WriteAll(converted.ExternalURLs["spotify"])
case spotify.FullTrack: case spotify.FullTrack:
go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second) go m.SendMessage("Copying link to "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
clipboard.WriteAll(converted.ExternalURLs["spotify"]) return clipboard.WriteAll(converted.ExternalURLs["spotify"])
} }
return nil return nil
} }
@ -343,11 +343,11 @@ func (m *mainModel) QueueItem() error {
}() }()
if m.mode == Queue { if m.mode == Queue {
go func() { go func() {
new_items, err := QueueView(m.commands) newItems, err := QueueView(m.commands)
if err != nil { if err != nil {
return return
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
}() }()
} }
return nil return nil
@ -364,11 +364,11 @@ func (m *mainModel) DeleteTrackFromPlaylist() error {
if err != nil { if err != nil {
m.SendMessage(err.Error(), 5*time.Second) m.SendMessage(err.Error(), 5*time.Second)
} }
new_items, err := PlaylistView(m.commands, m.playlist) newItems, err := PlaylistView(m.commands, m.playlist)
if err != nil { if err != nil {
m.SendMessage(err.Error(), 5*time.Second) m.SendMessage(err.Error(), 5*time.Second)
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
}() }()
return nil return nil
} }
@ -382,11 +382,11 @@ func (m *mainModel) SelectItem() error {
if err != nil { if err != nil {
m.SendMessage(err.Error(), 5*time.Second) m.SendMessage(err.Error(), 5*time.Second)
} }
new_items, err := QueueView(m.commands) newItems, err := QueueView(m.commands)
if err != nil { if err != nil {
m.SendMessage(err.Error(), 5*time.Second) m.SendMessage(err.Error(), 5*time.Second)
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
}() }()
case Search: case Search:
@ -394,150 +394,150 @@ func (m *mainModel) SelectItem() error {
switch item := m.list.SelectedItem().(mainItem).SpotifyItem.(type) { switch item := m.list.SelectedItem().(mainItem).SpotifyItem.(type) {
case *spotify.FullArtistPage: case *spotify.FullArtistPage:
m.mode = SearchArtists m.mode = SearchArtists
new_items, err := SearchArtistsView(m.commands, item) newItems, err := SearchArtistsView(m.commands, item)
if err != nil { if err != nil {
return err return err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
case *spotify.SimpleAlbumPage: case *spotify.SimpleAlbumPage:
m.mode = SearchAlbums m.mode = SearchAlbums
new_items, err := SearchAlbumsView(m.commands, item) newItems, err := SearchAlbumsView(m.commands, item)
if err != nil { if err != nil {
return err return err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
case *spotify.SimplePlaylistPage: case *spotify.SimplePlaylistPage:
m.mode = SearchPlaylists m.mode = SearchPlaylists
new_items, err := SearchPlaylistsView(m.commands, item) newItems, err := SearchPlaylistsView(m.commands, item)
if err != nil { if err != nil {
return err return err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
case *spotify.FullTrackPage: case *spotify.FullTrackPage:
m.mode = SearchTracks m.mode = SearchTracks
new_items, err := SearchTracksView(item) newItems, err := SearchTracksView(item)
if err != nil { if err != nil {
return err return err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
} }
case SearchArtists: case SearchArtists:
page = 1 page = 1
m.mode = SearchArtist m.mode = SearchArtist
m.artist = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleArtist) m.artist = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleArtist)
new_items, err := ArtistAlbumsView(m.artist.ID, m.commands) newItems, err := ArtistAlbumsView(m.artist.ID, m.commands)
if err != nil { if err != nil {
return err return err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
case SearchArtist: case SearchArtist:
page = 1 page = 1
m.mode = SearchArtistAlbum m.mode = SearchArtistAlbum
m.album = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleAlbum) m.album = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleAlbum)
new_items, err := AlbumTracksView(m.album.ID, m.commands) newItems, err := AlbumTracksView(m.album.ID, m.commands)
if err != nil { if err != nil {
return err return err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
case SearchAlbums: case SearchAlbums:
page = 1 page = 1
m.mode = SearchAlbum m.mode = SearchAlbum
m.album = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleAlbum) m.album = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleAlbum)
new_items, err := AlbumTracksView(m.album.ID, m.commands) newItems, err := AlbumTracksView(m.album.ID, m.commands)
if err != nil { if err != nil {
return err return err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
case SearchPlaylists: case SearchPlaylists:
page = 1 page = 1
m.mode = SearchPlaylist m.mode = SearchPlaylist
playlist := m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimplePlaylist) playlist := m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimplePlaylist)
m.playlist = playlist m.playlist = playlist
new_items, err := PlaylistView(m.commands, playlist) newItems, err := PlaylistView(m.commands, playlist)
if err != nil { if err != nil {
return err return err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
case Main: case Main:
page = 1 page = 1
switch item := m.list.SelectedItem().(mainItem).SpotifyItem.(type) { switch item := m.list.SelectedItem().(mainItem).SpotifyItem.(type) {
case spotify.Queue: case spotify.Queue:
m.mode = Queue m.mode = Queue
new_items, err := QueueView(m.commands) newItems, err := QueueView(m.commands)
if err != nil { if err != nil {
return err return err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
case *spotify.FullArtistCursorPage: case *spotify.FullArtistCursorPage:
m.mode = Artists m.mode = Artists
new_items, err := ArtistsView(m.commands) newItems, err := ArtistsView(m.commands)
if err != nil { if err != nil {
return err return err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
case *spotify.SavedAlbumPage: case *spotify.SavedAlbumPage:
m.mode = Albums m.mode = Albums
new_items, err := AlbumsView(m.commands) newItems, err := AlbumsView(m.commands)
if err != nil { if err != nil {
return err return err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
case spotify.SimplePlaylist: case spotify.SimplePlaylist:
m.mode = Playlist m.mode = Playlist
m.playlist = item m.playlist = item
new_items, err := PlaylistView(m.commands, item) newItems, err := PlaylistView(m.commands, item)
if err != nil { if err != nil {
return err return err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
case *spotify.SavedTrackPage: case *spotify.SavedTrackPage:
m.mode = Tracks m.mode = Tracks
new_items, err := SavedTracksView(m.commands) newItems, err := SavedTracksView(m.commands)
if err != nil { if err != nil {
return err return err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
} }
case Albums: case Albums:
page = 1 page = 1
m.mode = Album m.mode = Album
m.album = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleAlbum) m.album = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleAlbum)
new_items, err := AlbumTracksView(m.album.ID, m.commands) newItems, err := AlbumTracksView(m.album.ID, m.commands)
if err != nil { if err != nil {
return err return err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
case Artist: case Artist:
m.mode = ArtistAlbum m.mode = ArtistAlbum
m.album = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleAlbum) m.album = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleAlbum)
new_items, err := AlbumTracksView(m.album.ID, m.commands) newItems, err := AlbumTracksView(m.album.ID, m.commands)
if err != nil { if err != nil {
return err return err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
case Artists: case Artists:
m.mode = Artist m.mode = Artist
m.artist = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleArtist) m.artist = m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimpleArtist)
new_items, err := ArtistAlbumsView(m.artist.ID, m.commands) newItems, err := ArtistAlbumsView(m.artist.ID, m.commands)
if err != nil { if err != nil {
return err return err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
case Album, ArtistAlbum, SearchArtistAlbum, SearchAlbum: case Album, ArtistAlbum, SearchArtistAlbum, SearchAlbum:
pos := m.list.Cursor() + (m.list.Paginator.Page * m.list.Paginator.TotalPages) pos := m.list.Cursor() + (m.list.Paginator.Page * m.list.Paginator.TotalPages)
@ -579,17 +579,17 @@ func (m *mainModel) SelectItem() error {
} }
}() }()
m.mode = "main" m.mode = "main"
new_items, err := MainView(m.commands) newItems, err := MainView(m.commands)
if err != nil { if err != nil {
return err return err
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
} }
return nil return nil
} }
func (m *mainModel) Init() tea.Cmd { func (m *mainModel) Init() tea.Cmd {
main_updates = make(chan *mainModel) mainUpdates = make(chan *mainModel)
return Tick() return Tick()
} }
@ -602,10 +602,13 @@ func Tick() tea.Cmd {
} }
func (m *mainModel) TickPlayback() { func (m *mainModel) TickPlayback() {
playing, _ := m.commands.Client.PlayerCurrentlyPlaying(m.commands.Context) playing, _ := m.commands.Client().PlayerCurrentlyPlaying(m.commands.Context)
if playing != nil && playing.Playing && playing.Item != nil { if playing != nil && playing.Playing && playing.Item != nil {
if currentlyPlaying == nil || currentlyPlaying.Item == nil ||
currentlyPlaying.Item.ID != playing.Item.ID {
playbackContext, _ = m.getContext(playing)
}
currentlyPlaying = playing currentlyPlaying = playing
playbackContext, _ = m.getContext(playing)
} }
ticker := time.NewTicker(1 * time.Second) ticker := time.NewTicker(1 * time.Second)
quit := make(chan struct{}) quit := make(chan struct{})
@ -613,10 +616,14 @@ func (m *mainModel) TickPlayback() {
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
playing, _ := m.commands.Client.PlayerCurrentlyPlaying(m.commands.Context) m.commands.Log.Debug("TICKING PLAYBACK")
playing, _ := m.commands.Client().PlayerCurrentlyPlaying(m.commands.Context)
if playing != nil && playing.Playing && playing.Item != nil { if playing != nil && playing.Playing && playing.Item != nil {
if currentlyPlaying == nil || currentlyPlaying.Item == nil ||
currentlyPlaying.Item.ID != playing.Item.ID {
playbackContext, _ = m.getContext(playing)
}
currentlyPlaying = playing currentlyPlaying = playing
playbackContext, _ = m.getContext(playing)
} }
case <-quit: case <-quit:
ticker.Stop() ticker.Stop()
@ -658,26 +665,29 @@ func (m *mainModel) Typing(msg tea.KeyMsg) (bool, tea.Cmd) {
func (m *mainModel) getContext(playing *spotify.CurrentlyPlaying) (string, error) { func (m *mainModel) getContext(playing *spotify.CurrentlyPlaying) (string, error) {
context := playing.PlaybackContext context := playing.PlaybackContext
uri_split := strings.Split(string(context.URI), ":") uriSplit := strings.Split(string(context.URI), ":")
if len(uri_split) < 3 { if len(uriSplit) < 3 {
return "", fmt.Errorf("NO URI") return "", fmt.Errorf("NO URI")
} }
id := strings.Split(string(context.URI), ":")[2] id := strings.Split(string(context.URI), ":")[2]
switch context.Type { switch context.Type {
case "album": case "album":
album, err := m.commands.Client.GetAlbum(m.commands.Context, spotify.ID(id)) m.commands.Log.Debug("ALBUM CONTEXT")
album, err := m.commands.Client().GetAlbum(m.commands.Context, spotify.ID(id))
if err != nil { if err != nil {
return "", err return "", err
} }
return album.Name, nil return album.Name, nil
case "playlist": case "playlist":
playlist, err := m.commands.Client.GetPlaylist(m.commands.Context, spotify.ID(id)) m.commands.Log.Debug("PLAYLIST CONTEXT")
playlist, err := m.commands.Client().GetPlaylist(m.commands.Context, spotify.ID(id))
if err != nil { if err != nil {
return "", err return "", err
} }
return playlist.Name, nil return playlist.Name, nil
case "artist": case "artist":
artist, err := m.commands.Client.GetArtist(m.commands.Context, spotify.ID(id)) m.commands.Log.Debug("ARTIST CONTEXT")
artist, err := m.commands.Client().GetArtist(m.commands.Context, spotify.ID(id))
if err != nil { if err != nil {
return "", err return "", err
} }
@ -689,7 +699,7 @@ func (m *mainModel) getContext(playing *spotify.CurrentlyPlaying) (string, error
func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Update list items from LoadMore // Update list items from LoadMore
select { select {
case update := <-main_updates: case update := <-mainUpdates:
m.list.SetItems(update.list.Items()) m.list.SetItems(update.list.Items())
default: default:
} }
@ -711,11 +721,11 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.mode == Queue && len(m.list.Items()) != 0 { if m.mode == Queue && len(m.list.Items()) != 0 {
if m.list.Items()[0].(mainItem).SpotifyItem.(spotify.FullTrack).Name != playing.Item.Name { if m.list.Items()[0].(mainItem).SpotifyItem.(spotify.FullTrack).Name != playing.Item.Name {
go func() { go func() {
new_items, err := QueueView(m.commands) newItems, err := QueueView(m.commands)
if err != nil { if err != nil {
return return
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
}() }()
} }
} }
@ -746,7 +756,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if msg.String() == "c" { if msg.String() == "c" {
err := m.CopyToClipboard() err := m.CopyToClipboard()
if err != nil { if err != nil {
return m, tea.Quit go m.SendMessage(err.Error(), 5*time.Second)
} }
} }
if msg.String() == ">" { if msg.String() == ">" {
@ -796,11 +806,11 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// enter device selection // enter device selection
if msg.String() == "d" { if msg.String() == "d" {
m.mode = Devices m.mode = Devices
new_items, err := DeviceView(m.commands) newItems, err := DeviceView(m.commands)
if err != nil { if err != nil {
return m, tea.Quit return m, tea.Quit
} }
m.list.SetItems(new_items) m.list.SetItems(newItems)
m.list.ResetSelected() m.list.ResetSelected()
} }
// go back // go back
@ -837,18 +847,19 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
// handle mouse // handle mouse
case tea.MouseMsg: case tea.MouseButton:
if msg.Type == 5 { if msg == 5 {
m.list.CursorUp() m.list.CursorUp()
} }
if msg.Type == 6 { if msg == 6 {
m.list.CursorDown() m.list.CursorUp()
} }
// window size -1 to handle search bar // window size -1 to handle search bar
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
h, v := DocStyle.GetFrameSize() h, v := DocStyle.GetFrameSize()
m.list.SetSize(msg.Width-h, msg.Height-v-1) m.list.SetSize(msg.Width-h, msg.Height-v-1)
DocStyle.Width(msg.Width - h)
} }
// return // return

View File

@ -16,7 +16,7 @@ const regex = `<.*?>`
func DeviceView(commands *commands.Commander) ([]list.Item, error) { func DeviceView(commands *commands.Commander) ([]list.Item, error) {
items := []list.Item{} items := []list.Item{}
devices, err := commands.Client.PlayerDevices(commands.Context) devices, err := commands.Client().PlayerDevices(commands.Context)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -32,7 +32,7 @@ func DeviceView(commands *commands.Commander) ([]list.Item, error) {
func QueueView(commands *commands.Commander) ([]list.Item, error) { func QueueView(commands *commands.Commander) ([]list.Item, error) {
items := []list.Item{} items := []list.Item{}
tracks, err := commands.Client.GetQueue(commands.Context) tracks, err := commands.Client().GetQueue(commands.Context)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -63,7 +63,7 @@ func QueueView(commands *commands.Commander) ([]list.Item, error) {
func PlaylistView(commands *commands.Commander, playlist spotify.SimplePlaylist) ([]list.Item, error) { func PlaylistView(commands *commands.Commander, playlist spotify.SimplePlaylist) ([]list.Item, error) {
items := []list.Item{} items := []list.Item{}
playlistItems, err := commands.Client.GetPlaylistItems( playlistItems, err := commands.Client().GetPlaylistItems(
commands.Context, commands.Context,
playlist.ID, playlist.ID,
spotify.Limit(50), spotify.Limit(50),
@ -89,7 +89,7 @@ func PlaylistView(commands *commands.Commander, playlist spotify.SimplePlaylist)
func ArtistsView(commands *commands.Commander) ([]list.Item, error) { func ArtistsView(commands *commands.Commander) ([]list.Item, error) {
items := []list.Item{} items := []list.Item{}
artists, err := commands.Client.CurrentUsersFollowedArtists(commands.Context, spotify.Limit(50), spotify.Offset(0)) artists, err := commands.Client().CurrentUsersFollowedArtists(commands.Context, spotify.Limit(50), spotify.Offset(0))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -145,7 +145,7 @@ func SearchView(commands *commands.Commander, search string) ([]list.Item, *Sear
func AlbumsView(commands *commands.Commander) ([]list.Item, error) { func AlbumsView(commands *commands.Commander) ([]list.Item, error) {
items := []list.Item{} items := []list.Item{}
albums, err := commands.Client.CurrentUsersAlbums(commands.Context, spotify.Limit(50), spotify.Offset((page-1)*50)) albums, err := commands.Client().CurrentUsersAlbums(commands.Context, spotify.Limit(50), spotify.Offset((page-1)*50))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -171,7 +171,7 @@ func SearchPlaylistsView(commands *commands.Commander, playlists *spotify.Simple
for _, playlist := range playlists.Playlists { for _, playlist := range playlists.Playlists {
items = append(items, mainItem{ items = append(items, mainItem{
Name: playlist.Name, Name: playlist.Name,
Desc: stripHtmlRegex(playlist.Description), Desc: stripHTMLRegex(playlist.Description),
SpotifyItem: playlist, SpotifyItem: playlist,
}) })
} }
@ -249,7 +249,7 @@ func SearchTracksView(tracks *spotify.FullTrackPage) ([]list.Item, error) {
func SavedTracksView(commands *commands.Commander) ([]list.Item, error) { func SavedTracksView(commands *commands.Commander) ([]list.Item, error) {
items := []list.Item{} items := []list.Item{}
tracks, err := commands.Client.CurrentUsersTracks(commands.Context, spotify.Limit(50), spotify.Offset((page-1)*50)) tracks, err := commands.Client().CurrentUsersTracks(commands.Context, spotify.Limit(50), spotify.Offset((page-1)*50))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -267,29 +267,30 @@ func SavedTracksView(commands *commands.Commander) ([]list.Item, error) {
} }
func MainView(c *commands.Commander) ([]list.Item, error) { func MainView(c *commands.Commander) ([]list.Item, error) {
c.Log.Debug("SWITCHING TO MAIN VIEW")
wg := errgroup.Group{} wg := errgroup.Group{}
var saved_items *spotify.SavedTrackPage var savedItems *spotify.SavedTrackPage
var playlists *spotify.SimplePlaylistPage var playlists *spotify.SimplePlaylistPage
var artists *spotify.FullArtistCursorPage var artists *spotify.FullArtistCursorPage
var albums *spotify.SavedAlbumPage var albums *spotify.SavedAlbumPage
wg.Go(func() (err error) { wg.Go(func() (err error) {
saved_items, err = c.Client.CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(0)) savedItems, err = c.Client().CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(0))
return return
}) })
wg.Go(func() (err error) { wg.Go(func() (err error) {
playlists, err = c.Client.CurrentUsersPlaylists(c.Context, spotify.Limit(50), spotify.Offset(0)) playlists, err = c.Client().CurrentUsersPlaylists(c.Context, spotify.Limit(50), spotify.Offset(0))
return return
}) })
wg.Go(func() (err error) { wg.Go(func() (err error) {
artists, err = c.Client.CurrentUsersFollowedArtists(c.Context, spotify.Limit(50), spotify.Offset(0)) artists, err = c.Client().CurrentUsersFollowedArtists(c.Context, spotify.Limit(50), spotify.Offset(0))
return return
}) })
wg.Go(func() (err error) { wg.Go(func() (err error) {
albums, err = c.Client.CurrentUsersAlbums(c.Context, spotify.Limit(50), spotify.Offset(0)) albums, err = c.Client().CurrentUsersAlbums(c.Context, spotify.Limit(50), spotify.Offset(0))
return return
}) })
@ -299,11 +300,11 @@ func MainView(c *commands.Commander) ([]list.Item, error) {
} }
items := []list.Item{} items := []list.Item{}
if saved_items != nil && saved_items.Total != 0 { if savedItems != nil && savedItems.Total != 0 {
items = append(items, mainItem{ items = append(items, mainItem{
Name: "Saved Tracks", Name: "Saved Tracks",
Desc: fmt.Sprintf("%d saved songs", saved_items.Total), Desc: fmt.Sprintf("%d saved songs", savedItems.Total),
SpotifyItem: saved_items, SpotifyItem: savedItems,
}) })
} }
if albums != nil && albums.Total != 0 { if albums != nil && albums.Total != 0 {
@ -329,7 +330,7 @@ func MainView(c *commands.Commander) ([]list.Item, error) {
for _, playlist := range playlists.Playlists { for _, playlist := range playlists.Playlists {
items = append(items, mainItem{ items = append(items, mainItem{
Name: playlist.Name, Name: playlist.Name,
Desc: stripHtmlRegex(playlist.Description), Desc: stripHTMLRegex(playlist.Description),
SpotifyItem: playlist, SpotifyItem: playlist,
}) })
} }
@ -337,7 +338,7 @@ func MainView(c *commands.Commander) ([]list.Item, error) {
return items, nil return items, nil
} }
func stripHtmlRegex(s string) string { func stripHTMLRegex(s string) string {
r := regexp.MustCompile(regex) r := regexp.MustCompile(regex)
return r.ReplaceAllString(s, "") return r.ReplaceAllString(s, "")
} }

View File

@ -0,0 +1,102 @@
package tuitview
import (
"sync/atomic"
"git.asdf.cafe/abs3nt/gspot/src/components/commands"
"github.com/rivo/tview"
"github.com/zmb3/spotify/v2"
)
var (
tracksLoading = atomic.Bool{}
playlistsLoading = atomic.Bool{}
tracksPage = 1
playlistsPage = 1
)
func TuitView(cmd *commands.Commander) error {
tracksLoading.Store(false)
playlistsLoading.Store(false)
playlistsList := tview.NewList().ShowSecondaryText(false)
playlistsList.SetBorder(true).SetTitle("Playlists")
savedTracksList := tview.NewList().ShowSecondaryText(false)
savedTracksList.SetWrapAround(false)
savedTracksList.SetBorder(true).SetTitle("Tracks")
savedTracksList.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
go cmd.PlayLikedSongs(index)
})
flex := tview.NewFlex().AddItem(playlistsList, 0, 1, false).AddItem(savedTracksList, 0, 2, true)
playlists, err := cmd.Playlists(1)
if err != nil {
return err
}
for _, playlist := range playlists.Playlists {
playlistsList.AddItem(playlist.Name, "", 0, func() {
playlistTracksList := tview.NewList().ShowSecondaryText(false)
playlistTracksList.SetBorder(true).SetTitle(playlist.Name)
playlistTracksList.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
go cmd.PlaySongInPlaylist((*spotify.URI)(&secondaryText), &index)
})
tracks, err := cmd.PlaylistTracks(playlist.ID, 1)
if err != nil {
return
}
for _, track := range tracks.Items {
playlistTracksList.AddItem(track.Track.Track.Name+" - "+track.Track.Track.Artists[0].Name, string(playlist.URI), 0, nil)
}
flex.Clear()
flex.AddItem(playlistsList, 0, 1, false)
flex.AddItem(playlistTracksList, 0, 2, false)
})
}
tracks, err := cmd.TrackList(1)
if err != nil {
return err
}
for _, track := range tracks.Tracks {
savedTracksList.AddItem(track.Name+" - "+track.Artists[0].Name, "", 0, nil)
}
playlistsList.SetChangedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
if playlistsList.GetItemCount()%50 != 0 {
return
}
if playlistsList.GetItemCount()-index < 40 {
go func() {
if playlistsLoading.Load() {
return
}
playlistsLoading.Store(true)
defer playlistsLoading.Store(false)
playlistsPage++
newPlaylists, _ := cmd.Playlists(playlistsPage)
for _, playlist := range newPlaylists.Playlists {
savedTracksList.AddItem(playlist.Name, "", 0, nil)
}
}()
}
})
savedTracksList.SetChangedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
if savedTracksList.GetItemCount()%50 != 0 {
return
}
if savedTracksList.GetItemCount()-index < 40 {
go func() {
if tracksLoading.Load() {
return
}
tracksLoading.Store(true)
defer tracksLoading.Store(false)
tracksPage++
tracks, _ := cmd.TrackList(tracksPage)
for _, track := range tracks.Tracks {
savedTracksList.AddItem(track.Name+" - "+track.Artists[0].Name, "", 0, nil)
}
}()
}
})
if err := tview.NewApplication().EnableMouse(true).SetRoot(flex, true).Run(); err != nil {
return err
}
return nil
}

View File

@ -1,8 +1,10 @@
package config package config
type Config struct { type Config struct {
ClientId string `yaml:"client_id"` ClientID string `yaml:"client_id"`
ClientSecret string `yaml:"client_secret"` ClientSecret string `yaml:"client_secret"`
ClientSecretCmd string `yaml:"client_secret_cmd"` ClientSecretCmd string `yaml:"client_secret_cmd"`
Port string `yaml:"port"` Port string `yaml:"port"`
LogLevel string `yaml:"log_level" default:"info"`
LogOutput string `yaml:"log_output" default:"stdout"`
} }

View File

@ -13,18 +13,12 @@ import (
"github.com/zmb3/spotify/v2" "github.com/zmb3/spotify/v2"
spotifyauth "github.com/zmb3/spotify/v2/auth" spotifyauth "github.com/zmb3/spotify/v2/auth"
"go.uber.org/fx"
"golang.org/x/exp/slog" "golang.org/x/exp/slog"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"git.asdf.cafe/abs3nt/gspot/src/config" "git.asdf.cafe/abs3nt/gspot/src/config"
) )
type SpotifyClientResult struct {
fx.Out
Client *spotify.Client
}
var ( var (
auth *spotifyauth.Authenticator auth *spotifyauth.Authenticator
ch = make(chan *spotify.Client) ch = make(chan *spotify.Client)
@ -38,25 +32,25 @@ func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error)
return fn(req) return fn(req)
} }
func NewSpotifyClient(conf *config.Config) (c SpotifyClientResult, err error) { func GetClient(conf *config.Config) (c *spotify.Client, err error) {
if conf.ClientId == "" || (conf.ClientSecret == "" && conf.ClientSecretCmd == "") || conf.Port == "" { if conf.ClientID == "" || (conf.ClientSecret == "" && conf.ClientSecretCmd == "") || conf.Port == "" {
return SpotifyClientResult{}, fmt.Errorf("INVALID CONFIG") return nil, fmt.Errorf("INVALID CONFIG")
} }
if conf.ClientSecretCmd != "" { if conf.ClientSecretCmd != "" {
args := strings.Fields(conf.ClientSecretCmd) args := strings.Fields(conf.ClientSecretCmd)
cmd := args[0] cmd := args[0]
secret_command := exec.Command(cmd) secretCommand := exec.Command(cmd)
if len(args) > 1 { if len(args) > 1 {
secret_command.Args = args secretCommand.Args = args
} }
secret, err := secret_command.Output() secret, err := secretCommand.Output()
if err != nil { if err != nil {
panic(err) panic(err)
} }
conf.ClientSecret = strings.TrimSpace(string(secret)) conf.ClientSecret = strings.TrimSpace(string(secret))
} }
auth = spotifyauth.New( auth = spotifyauth.New(
spotifyauth.WithClientID(conf.ClientId), spotifyauth.WithClientID(conf.ClientID),
spotifyauth.WithClientSecret(conf.ClientSecret), spotifyauth.WithClientSecret(conf.ClientSecret),
spotifyauth.WithRedirectURL(fmt.Sprintf("http://localhost:%s/callback", conf.Port)), spotifyauth.WithRedirectURL(fmt.Sprintf("http://localhost:%s/callback", conf.Port)),
spotifyauth.WithScopes( spotifyauth.WithScopes(
@ -83,13 +77,13 @@ func NewSpotifyClient(conf *config.Config) (c SpotifyClientResult, err error) {
authFilePath := filepath.Join(configDir, "gspot/auth.json") authFilePath := filepath.Join(configDir, "gspot/auth.json")
authFile, err := os.Open(authFilePath) authFile, err := os.Open(authFilePath)
if err != nil { if err != nil {
return SpotifyClientResult{}, err return nil, err
} }
defer authFile.Close() defer authFile.Close()
tok := &oauth2.Token{} tok := &oauth2.Token{}
err = json.NewDecoder(authFile).Decode(tok) err = json.NewDecoder(authFile).Decode(tok)
if err != nil { if err != nil {
return SpotifyClientResult{}, err return nil, err
} }
authCtx := context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{ authCtx := context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{
Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) { Transport: roundTripperFunc(func(r *http.Request) (*http.Response, error) {
@ -98,20 +92,20 @@ func NewSpotifyClient(conf *config.Config) (c SpotifyClientResult, err error) {
}), }),
}) })
authClient := auth.Client(authCtx, tok) authClient := auth.Client(authCtx, tok)
client := spotify.New(authClient) client := spotify.New(authClient, spotify.WithRetry(true))
new_token, err := client.Token() newToken, err := client.Token()
if err != nil { if err != nil {
return SpotifyClientResult{}, err return nil, err
} }
out, err := json.MarshalIndent(new_token, "", " ") out, err := json.MarshalIndent(newToken, "", " ")
if err != nil { if err != nil {
return SpotifyClientResult{}, err return nil, err
} }
err = os.WriteFile(authFilePath, out, 0o600) err = os.WriteFile(authFilePath, out, 0o600)
if err != nil { if err != nil {
return SpotifyClientResult{}, fmt.Errorf("failed to save auth") return nil, fmt.Errorf("failed to save auth")
} }
return SpotifyClientResult{Client: client}, nil return client, nil
} }
// first start an HTTP server // first start an HTTP server
http.HandleFunc("/callback", completeAuth) http.HandleFunc("/callback", completeAuth)
@ -136,10 +130,10 @@ func NewSpotifyClient(conf *config.Config) (c SpotifyClientResult, err error) {
// use the client to make calls that require authorization // use the client to make calls that require authorization
user, err := client.CurrentUser(context.Background()) user, err := client.CurrentUser(context.Background())
if err != nil { if err != nil {
return SpotifyClientResult{}, err return nil, err
} }
slog.Info("AUTH", "You are logged in as:", user.ID) slog.Info("AUTH", "You are logged in as:", user.ID)
return SpotifyClientResult{Client: client}, nil return client, nil
} }
func completeAuth(w http.ResponseWriter, r *http.Request) { func completeAuth(w http.ResponseWriter, r *http.Request) {
@ -162,7 +156,7 @@ func completeAuth(w http.ResponseWriter, r *http.Request) {
slog.Error("AUTHENTICATOR", "failed to save auth", err) slog.Error("AUTHENTICATOR", "failed to save auth", err)
} }
// use the token to get an authenticated client // use the token to get an authenticated client
client := spotify.New(auth.Client(r.Context(), tok)) client := spotify.New(auth.Client(r.Context(), tok), spotify.WithRetry(true))
fmt.Fprintf(w, "Login Completed!") fmt.Fprintf(w, "Login Completed!")
ch <- client ch <- client
} }

18
src/services/config.go Normal file
View File

@ -0,0 +1,18 @@
package services
import (
"git.asdf.cafe/abs3nt/gunner"
"go.uber.org/fx"
"git.asdf.cafe/abs3nt/gspot/src/config"
)
var Config = fx.Options(
fx.Provide(
func() *config.Config {
c := &config.Config{}
gunner.LoadApp(c, "gspot")
return c
},
),
)