Files
musicplayer/src/musicplayer/musicplayer.gleam
Alexander Heldt ed69566f6f Plot layout on a grid
Redraw all x,y coordinates on screen Instead of using ANSI codes,
to be avoid clearing the screen which introduces flickering in TMUX
2025-12-20 14:05:25 +01:00

199 lines
5.0 KiB
Gleam

import gleam/erlang/process.{type Name, type Pid, type Subject}
import gleam/otp/actor
import gleam/result
import gleam/string
import musicplayer/control.{type Control}
import musicplayer/input/key.{type Key}
import musicplayer/logging/logging
import musicplayer/mpv/control as mpv_control
import musicplayer/time/time
import musicplayer/ui/control as ui_control
import musicplayer/ui/layout
type Mode {
Idle
Searching
}
type Input {
Input(capturing: Bool, content: String)
}
type State {
State(
mode: Mode,
input: Input,
ui: Subject(ui_control.Control),
mpv: Subject(mpv_control.Control),
)
}
pub fn new(
ui: Subject(ui_control.Control),
mpv: Subject(mpv_control.Control),
input_keys_name: Name(Key),
) -> Result(Pid, String) {
let input_keys = process.named_subject(input_keys_name)
let input = Input(False, "")
case
actor.new(State(Idle, input, ui, mpv))
|> actor.on_message(handle_message)
|> actor.start
{
Error(start_error) ->
Error("Could not start actor: " <> string.inspect(start_error))
Ok(actor.Started(pid:, data: musicplayer)) -> {
logging.log("musicplayer - started")
process.spawn(fn() {
let assert Ok(_) = process.register(process.self(), input_keys_name)
handle_key(musicplayer, input_keys)
})
process.spawn(fn() { update_playback_time_loop(mpv, ui, 250) })
Ok(pid)
}
}
}
fn handle_message(state: State, control: Control) -> actor.Next(State, Control) {
case control {
control.Search -> {
logging.log("musicplayer - initiating search")
update_search(state.ui, "searching: ")
actor.continue(
State(
..state,
mode: Searching,
input: Input(..state.input, capturing: True),
),
)
}
control.Raw(content) -> {
logging.log("musicplayer - recieved raw input: " <> content)
let content = case state.mode {
Idle -> state.input.content
Searching -> {
let updated = state.input.content <> content
update_search(state.ui, "searching: " <> updated)
updated
}
}
actor.continue(State(..state, input: Input(..state.input, content:)))
}
control.Backspace -> {
logging.log("musicplayer - recieved backspace")
let content = case state.mode {
Idle -> state.input.content
Searching -> string.drop_end(state.input.content, 1)
}
actor.continue(State(..state, input: Input(..state.input, content:)))
}
control.Return -> {
logging.log(
"musicplayer - recieved return. `input.capture`: "
<> "'"
<> state.input.content
<> "'",
)
// Note: state.input.content is now the final input, use it
// before it is reset
case state.mode {
Idle -> Nil
Searching -> update_search(state.ui, "")
}
actor.continue(
State(..state, mode: Idle, input: Input(capturing: False, content: "")),
)
}
control.TogglePlayPause -> {
logging.log("musicplayer - toggling play/pause")
process.send(state.mpv, mpv_control.TogglePlayPause)
update_playback_time(state.mpv, state.ui)
actor.continue(state)
}
control.Exit -> {
logging.log("musicplayer - initiating musicplayer shutdown")
// Close `mpv` socket
process.call(state.mpv, 1000, fn(reply_to) { mpv_control.Exit(reply_to) })
// Reset terminal state (show cursor etc.)
process.call(state.ui, 1000, fn(reply_to) { ui_control.Exit(reply_to) })
logging.log("musicplayer - stopped")
actor.stop()
}
}
}
fn update_playback_time_loop(
mpv: Subject(mpv_control.Control),
ui: Subject(ui_control.Control),
interval_ms: Int,
) {
process.sleep(interval_ms)
// TODO only update if state is playing
update_playback_time(mpv, ui)
update_playback_time_loop(mpv, ui, interval_ms)
}
fn update_playback_time(
mpv: Subject(mpv_control.Control),
ui: Subject(ui_control.Control),
) -> Nil {
case
process.call(mpv, 1000, fn(reply_to) {
mpv_control.GetPlaybackTime(reply_to)
})
{
Error(err) ->
process.send(
ui,
ui_control.UpdateState(
layout.PlaybackTime,
"playback time: N/A (err: " <> err.details <> ")",
),
)
Ok(mpv_control.PlaybackTime(data: playback_time)) ->
process.send(
ui,
ui_control.UpdateState(
layout.PlaybackTime,
"playback time: " <> time.to_duration_string(playback_time),
),
)
}
}
fn update_search(ui: Subject(ui_control.Control), content: String) -> Nil {
process.send(ui, ui_control.UpdateState(layout.Search, content))
}
/// `handle_key` listens to a subject onto which `input` will send messages with `Key`s
fn handle_key(musicplayer: Subject(Control), input_keys: Subject(Key)) -> Nil {
let _ =
process.new_selector()
|> process.select(input_keys)
|> process.selector_receive_forever
|> control.from_key
|> result.map(process.send(musicplayer, _))
handle_key(musicplayer, input_keys)
}