Redraw all x,y coordinates on screen Instead of using ANSI codes, to be avoid clearing the screen which introduces flickering in TMUX
199 lines
5.0 KiB
Gleam
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)
|
|
}
|