Files
musicplayer/src/mpv/mpv.gleam
Alexander Heldt 747f76a584 Add ability to listen (tap) the input
By doing something like
```
fn input_output_loop(input_output: Subject(List(String))) -> Nil {
  let output = process.receive_forever(input_output)

  echo output

  input_output_loop(input_output)
}
```
2025-11-19 18:20:36 +01:00

111 lines
3.2 KiB
Gleam

import gleam/erlang/process.{type Subject}
import gleam/float
import gleam/otp/actor
import gleam/result
import gleam/string
import input/key.{type Key}
import mpv/control.{type Control}
import tcp/reason
import tcp/tcp.{type Socket}
type State(socket, inject_input, tap_input, exit) {
State(
socket: Socket,
inject_input: Subject(Key),
tap_input: Subject(List(String)),
exit: Subject(Nil),
)
}
pub fn new(exit: Subject(Nil)) -> Result(Nil, String) {
// TODO start up mvp here, currently hi-jacking `naviterm`s socket
let socket_path = "/tmp/naviterm_mpv"
case tcp.connect(socket_path) {
Error(r) -> Error("Could not connect to mpv: " <> reason.to_string(r))
Ok(socket) -> {
// `inject_input` is created by name to allow the process that
// owns `read_input` to be able to register it, while the agent
// also have a reference to it to be able to inject input
let inject_input_name = process.new_name("inject_input")
let inject_input = process.named_subject(inject_input_name)
let tap_input_name = process.new_name("tap_input")
let tap_input = process.named_subject(tap_input_name)
case
actor.new(State(socket, inject_input, tap_input, exit))
|> actor.on_message(handle_message)
|> actor.start
{
Error(start_error) ->
Error("Could not start actor: " <> string.inspect(start_error))
Ok(actor.Started(data:, ..)) -> {
echo "waiting for input"
key.start_raw_shell()
process.spawn(fn() {
let assert Ok(_) =
process.register(process.self(), inject_input_name)
read_input(data, inject_input, tap_input)
})
Ok(Nil)
}
}
}
}
}
fn handle_message(
state: State(socket, inject, input_output, exit),
control: Control,
) -> actor.Next(State(socket, inject, input_output, exit), Control) {
case control {
control.TogglePlayPause -> {
echo "toggling play/pause"
let _ =
result.map_error(control.toggle_play_pause(state.socket), fn(err) {
echo "Could not toggle play/pause: " <> err.details
})
let _ =
result.map(control.get_playback_time(state.socket), fn(playback) {
echo "playback: " <> float.to_string(playback.data)
})
actor.continue(state)
}
control.Exit -> {
process.send(state.exit, Nil)
actor.stop()
}
}
}
/// `read_input` operates by reading from input until a `Key` can be created.
/// It is possible to create a `Key` without the users input by sending
/// messages to `inject_input` which will initialize the "input to key" sequence.
/// This is useful to ultimately create a `Control` without the user having to
/// input all of the character(s) needed.
fn read_input(
subject: Subject(Control),
inject_input: Subject(Key),
tap_input: Subject(List(String)),
) -> Nil {
let buffer = case process.receive(inject_input, 1) {
Ok(key.Continue(buffer)) -> buffer
Ok(_) | Error(_) -> []
}
let _ =
key.read_input_until_key(buffer, tap_input)
|> control.from_key
|> result.map(process.send(subject, _))
read_input(subject, inject_input, tap_input)
}