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) }