diff --git a/src/input/input.gleam b/src/input/input.gleam index 3e8cace..f74a28b 100644 --- a/src/input/input.gleam +++ b/src/input/input.gleam @@ -1,59 +1,50 @@ import gleam/erlang/process.{type Name, type Subject} -import gleam/list -import gleam/option.{type Option, None, Some} import input/key.{type Key} -pub type Listener { - InputListener(final: Subject(Key), tap: Option(Subject(List(String)))) -} - /// `new` accepts a list of listeners that are composed of two subjects; /// - one to get the final `Key` and /// - one to tap the input as it is read from i/o /// and /// - a subject name that is used to create a `Subject` that other processes /// know they can inject a `Key` into the input with -pub fn new(listeners: List(Listener), inject_input_name: Name(Key)) -> Nil { +pub fn new( + ui_input_keys_name: Name(Key), + ui_input_stream_name: Name(List(String)), + input_ui_inject_name: Name(Key), +) -> Nil { let _ = process.spawn(fn() { - let inject_input: Subject(Key) = process.named_subject(inject_input_name) - let assert Ok(_) = process.register(process.self(), inject_input_name) + let ui_input_keys: Subject(Key) = + process.named_subject(ui_input_keys_name) + let ui_input_stream: Subject(List(String)) = + process.named_subject(ui_input_stream_name) - // Extract all finals and taps (that are defined) - let #(finals, taps) = - list.fold(listeners, #([], []), fn(acc, listener) { - let #(finals, taps) = acc + let input_ui_inject_input: Subject(Key) = + process.named_subject(input_ui_inject_name) + let assert Ok(_) = process.register(process.self(), input_ui_inject_name) - let finals = [listener.final, ..finals] - - let taps = case listener.tap { - Some(t) -> [t, ..taps] - None -> taps - } - - #(finals, taps) - }) - - read_input(finals, taps, inject_input) + read_input(ui_input_keys, ui_input_stream, input_ui_inject_input) }) + echo "waiting for input" + key.start_raw_shell() + Nil } fn read_input( - finals: List(Subject(Key)), - taps: List(Subject(List(String))), - inject_input: Subject(Key), + ui_input_keys: Subject(Key), + ui_input_stream: Subject(List(String)), + input_ui_inject: Subject(Key), ) -> Nil { - let buffer = case process.receive(inject_input, 1) { + let buffer = case process.receive(input_ui_inject, 1) { Ok(key.Continue(buffer)) -> buffer Ok(_) | Error(_) -> [] } - let _ = - key.read_input_until_key(buffer, taps) - |> fn(k) { list.each(finals, process.send(_, k)) } + key.read_input_until_key(buffer, ui_input_stream) + |> process.send(ui_input_keys, _) - read_input(finals, taps, inject_input) + read_input(ui_input_keys, ui_input_stream, input_ui_inject) } diff --git a/src/input/key.gleam b/src/input/key.gleam index ebb7c2f..4980922 100644 --- a/src/input/key.gleam +++ b/src/input/key.gleam @@ -62,7 +62,7 @@ pub fn start_raw_shell() { pub fn read_input_until_key( l: List(String), - taps: List(Subject(List(String))), + ui_input_stream: Subject(List(String)), ) -> Key { case internal_input.read_input() @@ -71,8 +71,8 @@ pub fn read_input_until_key( |> from_list { Continue(l) -> { - list.each(taps, process.send(_, l)) - read_input_until_key(l, taps) + process.send(ui_input_stream, l) + read_input_until_key(l, ui_input_stream) } k -> k } diff --git a/src/mpv/control.gleam b/src/mpv/control.gleam index c33e03a..fba6db4 100644 --- a/src/mpv/control.gleam +++ b/src/mpv/control.gleam @@ -2,7 +2,6 @@ import gleam/json import gleam/result import gleam/string -import input/key.{type Key} import mpv/internal as internal_control import tcp/reason.{type Reason} import tcp/tcp.{type Socket} @@ -17,21 +16,6 @@ pub type ControlError { ControlError(details: String) } -pub fn from_key(key: Key) -> Result(Control, Nil) { - case key { - key.Char(char) -> char_control(char) - _ -> Error(Nil) - } -} - -fn char_control(char: String) -> Result(Control, Nil) { - case char { - " " -> Ok(TogglePlayPause) - "q" -> Ok(Exit) - _ -> Error(Nil) - } -} - pub fn toggle_play_pause(socket: Socket) -> Result(Nil, ControlError) { let command = json.object([#("command", json.array(["cycle", "pause"], of: json.string))]) diff --git a/src/mpv/mpv.gleam b/src/mpv/mpv.gleam index 5b256a9..a7c3558 100644 --- a/src/mpv/mpv.gleam +++ b/src/mpv/mpv.gleam @@ -1,49 +1,49 @@ -import gleam/erlang/process.{type Subject} +import gleam/erlang/process.{type Name, type Subject} import gleam/float -import gleam/option.{None} import gleam/otp/actor import gleam/result import gleam/string -import input/input.{type Listener, InputListener} -import input/key.{type Key} -import mpv/control.{type Control} +import mpv/control as mpv_control import tcp/reason import tcp/tcp.{type Socket} +import ui/control as ui_control -type State(socket, exit) { - State(socket: Socket, exit: Subject(Nil)) +type State(socket, mpv_ui, ui_mpv) { + State( + socket: Socket, + mpv_ui: Subject(ui_control.Control), + ui_mpv: Subject(mpv_control.Control), + ) } -pub fn new(exit: Subject(Nil)) -> Result(Listener, String) { +pub fn new( + mpv_ui_name: Name(ui_control.Control), + ui_mpv_name: Name(mpv_control.Control), +) -> Result(Nil, String) { // TODO start up mvp here, currently hi-jacking `naviterm`s socket let socket_path = "/tmp/naviterm_mpv" + let mpv_ui: Subject(ui_control.Control) = process.named_subject(mpv_ui_name) + let ui_mpv: Subject(mpv_control.Control) = process.named_subject(ui_mpv_name) + case tcp.connect(socket_path) { Error(r) -> Error("Could not connect to mpv: " <> reason.to_string(r)) Ok(socket) -> { - let final_input_name = process.new_name("mpv_final_input") - let final_input: Subject(Key) = process.named_subject(final_input_name) - case - actor.new(State(socket, exit)) + actor.new(State(socket, mpv_ui, ui_mpv)) |> 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() - + Ok(actor.Started(data: mpv, ..)) -> { process.spawn(fn() { - let assert Ok(_) = - process.register(process.self(), final_input_name) - - handle_key(final_input, data) + let assert Ok(_) = process.register(process.self(), ui_mpv_name) + handle_ui_control(mpv, ui_mpv) }) - Ok(InputListener(final: final_input, tap: None)) + Ok(Nil) } } } @@ -51,39 +51,41 @@ pub fn new(exit: Subject(Nil)) -> Result(Listener, String) { } fn handle_message( - state: State(socket, exit), - control: Control, -) -> actor.Next(State(socket, exit), Control) { + state: State(socket, mpv_ui, ui_mpv), + control: mpv_control.Control, +) -> actor.Next(State(socket, mpv_ui, ui_mpv), mpv_control.Control) { case control { - control.TogglePlayPause -> { + mpv_control.TogglePlayPause -> { echo "toggling play/pause" let _ = - result.map_error(control.toggle_play_pause(state.socket), fn(err) { + result.map_error(mpv_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) { + result.map(mpv_control.get_playback_time(state.socket), fn(playback) { echo "playback: " <> float.to_string(playback.data) }) + process.send(state.mpv_ui, ui_control.Ack) + actor.continue(state) } - control.Exit -> { - process.send(state.exit, Nil) + mpv_control.Exit -> { + // TODO close sockets actor.stop() } } } -/// `handle_key` listens to a subject onto which `input` will send messages with -/// parsed `Key`s which will be mapped to `Control`s (if possible) -fn handle_key(final_input: Subject(Key), subject: Subject(Control)) -> Nil { - let _ = - process.receive_forever(final_input) - |> control.from_key - |> result.map(process.send(subject, _)) +fn handle_ui_control( + mpv: Subject(mpv_control.Control), + ui_mpv: Subject(mpv_control.Control), +) { + let control = process.receive_forever(ui_mpv) + echo "handle_ui_control: " <> string.inspect(control) + process.send(mpv, control) - handle_key(final_input, subject) + handle_ui_control(mpv, ui_mpv) } diff --git a/src/musicplayer.gleam b/src/musicplayer.gleam index 98151d8..babbf6f 100644 --- a/src/musicplayer.gleam +++ b/src/musicplayer.gleam @@ -1,22 +1,45 @@ import gleam/erlang/process.{type Name} -import input/input.{type Listener} +import input/input import input/key.{type Key} +import mpv/control as mpv_control import mpv/mpv +import ui/control as ui_control +import ui/ui pub fn main() -> Nil { let exit = process.new_subject() + // `ui_input_keys` is owned by `ui`, this is where input sends keys + let ui_input_keys_name: Name(Key) = process.new_name("ui_input_keys") + // `ui_input_stream` is owned by `ui`, thi sis where input is streamed + let ui_input_stream_name: Name(List(String)) = + process.new_name("ui_input_stream") + // `inject_input` is created by name to allow the `input` process that // owns `read_input` to be able to register and receive from it, // while the any other processes can use the name reference to // inject input - let inject_input_name: Name(Key) = process.new_name("inject_input") - let assert Ok(mpv_listener) = mpv.new(exit) + // `input_ui_inject` is owned by `input`, this is where `ui` injects `Key` + let input_ui_inject_name: Name(Key) = process.new_name("input_ui_inject_keys") - let listeners: List(Listener) = [mpv_listener] + // `ui_mpv` is owned by `ui`, this is where `ui` asks `mpv` for things + let ui_mpv_name: Name(mpv_control.Control) = process.new_name("ui_mpv") + // `mpv_ui` is owned by mpv and this is where mpv responds to ui + let mpv_ui_name: Name(ui_control.Control) = process.new_name("mpv_ui") + + let assert Ok(_) = + ui.new( + ui_mpv_name, + mpv_ui_name, + ui_input_keys_name, + ui_input_stream_name, + exit, + ) + + let assert Ok(_) = mpv.new(mpv_ui_name, ui_mpv_name) + input.new(ui_input_keys_name, ui_input_stream_name, input_ui_inject_name) - input.new(listeners, inject_input_name) process.receive_forever(exit) } diff --git a/src/ui/control.gleam b/src/ui/control.gleam new file mode 100644 index 0000000..c17f954 --- /dev/null +++ b/src/ui/control.gleam @@ -0,0 +1,24 @@ +import input/key.{type Key} + +pub type Control { + Ack + + TogglePlayPause + + Exit +} + +pub fn from_key(key: Key) -> Result(Control, Nil) { + case key { + key.Char(char) -> char_control(char) + _ -> Error(Nil) + } +} + +fn char_control(char: String) -> Result(Control, Nil) { + case char { + " " -> Ok(TogglePlayPause) + "q" -> Ok(Exit) + _ -> Error(Nil) + } +} diff --git a/src/ui/ui.gleam b/src/ui/ui.gleam new file mode 100644 index 0000000..0398bd4 --- /dev/null +++ b/src/ui/ui.gleam @@ -0,0 +1,122 @@ +import gleam/erlang/process.{type Name, type Subject} +import gleam/otp/actor +import gleam/result +import gleam/string + +import input/key.{type Key} +import mpv/control as mpv_control +import ui/control as ui_control + +pub type State(ui_mpv, mpv_ui, ui_input_keys, ui_input_stream, exit) { + State( + ui_mpv: Subject(mpv_control.Control), + mpv_ui: Subject(ui_control.Control), + ui_input_keys: Subject(Key), + ui_input_stream: Subject(List(String)), + exit: Subject(Nil), + ) +} + +pub fn new( + ui_mpv_name: Name(mpv_control.Control), + mpv_ui_name: Name(ui_control.Control), + ui_input_keys_name: Name(Key), + ui_input_stream_name: Name(List(String)), + exit: Subject(Nil), +) -> Result(Nil, String) { + let ui_mpv: Subject(mpv_control.Control) = process.named_subject(ui_mpv_name) + let mpv_ui: Subject(ui_control.Control) = process.named_subject(mpv_ui_name) + + let ui_input_keys: Subject(Key) = process.named_subject(ui_input_keys_name) + let ui_input_stream: Subject(List(String)) = + process.named_subject(ui_input_stream_name) + + case + actor.new(State(ui_mpv, mpv_ui, ui_input_keys, ui_input_stream, exit)) + |> actor.on_message(handle_message) + |> actor.start + { + Error(start_error) -> + Error("Could not start ui: " <> string.inspect(start_error)) + Ok(actor.Started(data: ui, ..)) -> { + echo "ui started" + + process.spawn(fn() { + let assert Ok(_) = process.register(process.self(), ui_input_keys_name) + handle_key(ui, ui_input_keys) + }) + + process.spawn(fn() { + let assert Ok(_) = process.register(process.self(), mpv_ui_name) + handle_mpv_control(ui, mpv_ui) + }) + + process.spawn(fn() { + let assert Ok(_) = + process.register(process.self(), ui_input_stream_name) + temp_input_stream(ui_input_stream) + }) + + Ok(Nil) + } + } +} + +fn handle_message( + state: State(ui_mpv, mpv_ui, ui_input_keys, ui_input_stream, exit), + control: ui_control.Control, +) -> actor.Next( + State(ui_mpv, mpv_ui, ui_input_keys, ui_input_stream, exit), + ui_control.Control, +) { + case control { + ui_control.Ack -> { + echo "ack! use this to re-render?" + actor.continue(state) + } + ui_control.TogglePlayPause -> { + process.send(state.mpv_ui, ui_control.TogglePlayPause) + case process.receive(state.mpv_ui, 5000) { + Error(_) -> echo "mpv not responding: could not toggle pause :(" + Ok(_) -> echo "toggled pause" + } + + actor.continue(state) + } + ui_control.Exit -> { + // TODO call `mpv` to exit, wait for response + process.send(state.exit, Nil) + actor.stop() + } + } +} + +/// `handle_key` listens to a subject onto which `input` will send messages with +/// parsed `Key`s which will be mapped to `Control`s (if possible) +fn handle_key( + ui: Subject(ui_control.Control), + ui_input_keys: Subject(Key), +) -> Nil { + let _ = + process.receive_forever(ui_input_keys) + |> ui_control.from_key + |> result.map(process.send(ui, _)) + + handle_key(ui, ui_input_keys) +} + +fn handle_mpv_control( + ui: Subject(ui_control.Control), + mpv_ui: Subject(ui_control.Control), +) { + let control = process.receive_forever(mpv_ui) + process.send(ui, control) + + handle_mpv_control(ui, mpv_ui) +} + +fn temp_input_stream(ui_input_stream: Subject(List(String))) -> Nil { + let stream = process.receive_forever(ui_input_stream) + echo "input stream: " <> string.inspect(stream) + temp_input_stream(ui_input_stream) +} diff --git a/test/mpv/control_test.gleam b/test/mpv/control_test.gleam index 67b03a4..702cbbe 100644 --- a/test/mpv/control_test.gleam +++ b/test/mpv/control_test.gleam @@ -1,29 +1,11 @@ -import gleam/list import gleeunit -import input/key.{type Key, Char} -import mpv/control.{type Control} import mpv/internal as control_internal pub fn main() -> Nil { gleeunit.main() } -type TestCase { - TestCase(key: Key, expected: Result(Control, Nil)) -} - -pub fn control_from_key_test() { - let test_cases = [ - TestCase(Char(" "), Ok(control.TogglePlayPause)), - TestCase(Char("q"), Ok(control.Exit)), - ] - - list.each(test_cases, fn(tc) { - assert tc.expected == control.from_key(tc.key) - }) -} - pub fn parse_playback_time_test() { let json_string = "{\"data\":\"123.456789\",\"request_id\":0,\"error\":\"success\"}\n" diff --git a/test/ui/ui_test.gleam b/test/ui/ui_test.gleam new file mode 100644 index 0000000..deb1b19 --- /dev/null +++ b/test/ui/ui_test.gleam @@ -0,0 +1,24 @@ +import gleam/list +import gleeunit + +import input/key.{type Key, Char} +import ui/control.{type Control} + +pub fn main() -> Nil { + gleeunit.main() +} + +type TestCase { + TestCase(key: Key, expected: Result(Control, Nil)) +} + +pub fn control_from_key_test() { + let test_cases = [ + TestCase(Char(" "), Ok(control.TogglePlayPause)), + TestCase(Char("q"), Ok(control.Exit)), + ] + + list.each(test_cases, fn(tc) { + assert tc.expected == control.from_key(tc.key) + }) +}