Simplify input capture
Instead of "injecting" characters into the input stream, the input stream is now forwarded to the `musicplayer`. It has will have to decide what to do with the stream, e.g. by setting the "mode" to something that captures the input stream and acts upon it
This commit is contained in:
@@ -8,15 +8,13 @@ import musicplayer/ui/ui
|
||||
|
||||
pub fn main() -> Nil {
|
||||
let input_keys_name: Name(Key) = process.new_name("input_keys")
|
||||
let input_inject_name: Name(Key) = process.new_name("input_inject_keys")
|
||||
|
||||
input.new(input_keys_name, input_inject_name)
|
||||
input.new(input_keys_name)
|
||||
|
||||
let assert Ok(ui) = ui.new()
|
||||
let assert Ok(mpv) = mpv.new()
|
||||
|
||||
let exit = process.new_subject()
|
||||
let assert Ok(_) =
|
||||
musicplayer.new(ui, mpv, input_keys_name, input_inject_name, exit)
|
||||
let assert Ok(_) = musicplayer.new(ui, mpv, input_keys_name, exit)
|
||||
process.receive_forever(exit)
|
||||
}
|
||||
|
||||
@@ -2,26 +2,29 @@ import musicplayer/input/key.{type Key}
|
||||
|
||||
pub type Control {
|
||||
TogglePlayPause
|
||||
Search
|
||||
|
||||
Input(String)
|
||||
Search(List(String))
|
||||
Raw(String)
|
||||
Return
|
||||
Backspace
|
||||
|
||||
Exit
|
||||
}
|
||||
|
||||
pub fn from_key(key: Key) -> Result(Control, Nil) {
|
||||
case key {
|
||||
key.Input(content) -> Ok(Input(content))
|
||||
key.Char(char) -> char_control(char)
|
||||
key.Return -> Ok(Return)
|
||||
key.Backspace -> Ok(Backspace)
|
||||
key.Char(char) -> Ok(char_control(char))
|
||||
_ -> Error(Nil)
|
||||
}
|
||||
}
|
||||
|
||||
fn char_control(char: String) -> Result(Control, Nil) {
|
||||
fn char_control(char: String) -> Control {
|
||||
case char {
|
||||
" " -> Ok(TogglePlayPause)
|
||||
"/" -> Ok(Search([]))
|
||||
"q" -> Ok(Exit)
|
||||
_ -> Error(Nil)
|
||||
" " -> TogglePlayPause
|
||||
"/" -> Search
|
||||
"q" -> Exit
|
||||
_ -> Raw(char)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,12 @@ import gleam/erlang/process.{type Name}
|
||||
|
||||
import musicplayer/input/key.{type Key}
|
||||
|
||||
/// `new` accepts two named subjects:
|
||||
/// - one to send the input to, and
|
||||
/// - one to receive injected input from
|
||||
///
|
||||
/// The subject for input injection is used 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
|
||||
pub fn new(input_keys_name: Name(Key), input_inject_name: Name(Key)) -> Nil {
|
||||
/// `new` accepts a subject that all input will be sent to
|
||||
pub fn new(input_keys_name: Name(Key)) -> Nil {
|
||||
let _ =
|
||||
process.spawn(fn() {
|
||||
let input_keys = process.named_subject(input_keys_name)
|
||||
let input_inject = process.named_subject(input_inject_name)
|
||||
|
||||
let assert Ok(_) = process.register(process.self(), input_inject_name)
|
||||
|
||||
key.read_input_until_key(input_keys, input_inject)
|
||||
key.read_input_until_key(input_keys)
|
||||
})
|
||||
|
||||
key.start_raw_shell()
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import gleam/erlang/atom
|
||||
import gleam/erlang/process.{type Subject}
|
||||
import gleam/list
|
||||
import gleam/string
|
||||
|
||||
import musicplayer/input/internal as internal_input
|
||||
|
||||
pub type Key {
|
||||
Continue(buffer: List(String))
|
||||
Char(String)
|
||||
|
||||
Input(String)
|
||||
Left
|
||||
Right
|
||||
Up
|
||||
Down
|
||||
|
||||
Continue(buffer: List(String))
|
||||
Return
|
||||
Backspace
|
||||
|
||||
Unknown
|
||||
}
|
||||
|
||||
@@ -23,8 +24,9 @@ pub const esc = "\u{001B}"
|
||||
// control sequence introducer
|
||||
pub const csi = "["
|
||||
|
||||
// input introducer
|
||||
pub const input_introducer = "::"
|
||||
pub const return = "\r"
|
||||
|
||||
pub const backspace = "\u{007F}"
|
||||
|
||||
pub fn from_list(l: List(String)) -> Key {
|
||||
case l {
|
||||
@@ -35,18 +37,9 @@ pub fn from_list(l: List(String)) -> Key {
|
||||
|
||||
[e, c] if e == esc && c == csi -> Continue(l)
|
||||
|
||||
[ci] | [ci, _] if ci == input_introducer -> Continue(l)
|
||||
[ii, cmd, tail] if ii == input_introducer -> {
|
||||
case tail {
|
||||
// Return
|
||||
"\r" -> Input(cmd)
|
||||
// Backspace
|
||||
"\u{007F}" -> Continue([ii, string.drop_end(cmd, 1)])
|
||||
_ -> Continue([ii, cmd <> tail])
|
||||
}
|
||||
}
|
||||
|
||||
[e] if e == esc -> Continue(l)
|
||||
[bs] if bs == backspace -> Backspace
|
||||
[rtn] if rtn == return -> Return
|
||||
[char] -> Char(char)
|
||||
|
||||
[] -> Continue([])
|
||||
@@ -60,35 +53,24 @@ pub fn start_raw_shell() {
|
||||
internal_input.shell_start_interactive(#(no_shell, raw))
|
||||
}
|
||||
|
||||
pub fn read_input_until_key(
|
||||
input_keys: Subject(Key),
|
||||
input_inject: Subject(Key),
|
||||
) -> Nil {
|
||||
case process.receive(input_inject, 1) {
|
||||
Ok(Continue(buffer)) -> buffer
|
||||
Ok(_) | Error(_) -> []
|
||||
}
|
||||
|> forward_input_to(input_keys)
|
||||
|
||||
read_input_until_key(input_keys, input_inject)
|
||||
pub fn read_input_until_key(input_keys: Subject(Key)) -> Nil {
|
||||
read_input_until_key_loop([], input_keys)
|
||||
}
|
||||
|
||||
pub fn forward_input_to(l: List(String), input_keys: Subject(Key)) -> Nil {
|
||||
pub fn read_input_until_key_loop(
|
||||
l: List(String),
|
||||
input_keys: Subject(Key),
|
||||
) -> Nil {
|
||||
case
|
||||
internal_input.read_input()
|
||||
|> list.wrap
|
||||
|> list.append(l, _)
|
||||
|> from_list
|
||||
{
|
||||
Continue(l) -> {
|
||||
case l {
|
||||
[ii, cmd] if ii == input_introducer -> {
|
||||
process.send(input_keys, Continue([cmd]))
|
||||
forward_input_to(l, input_keys)
|
||||
}
|
||||
_ -> forward_input_to(l, input_keys)
|
||||
}
|
||||
Continue(l) -> read_input_until_key_loop(l, input_keys)
|
||||
k -> {
|
||||
process.send(input_keys, k)
|
||||
read_input_until_key_loop([], input_keys)
|
||||
}
|
||||
k -> process.send(input_keys, k)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,6 @@ fn handle_message(
|
||||
) -> actor.Next(State(socket), Control) {
|
||||
case control {
|
||||
control.TogglePlayPause -> {
|
||||
echo "mpv: toggling play/pause"
|
||||
|
||||
let _ =
|
||||
result.map_error(control.toggle_play_pause(state.socket), fn(err) {
|
||||
echo "Could not toggle play/pause: " <> err.details
|
||||
|
||||
@@ -10,11 +10,21 @@ import musicplayer/mpv/control as mpv_control
|
||||
import musicplayer/ui/control as ui_control
|
||||
import musicplayer/ui/layout
|
||||
|
||||
type State(ui, mpv, input_inject, exit) {
|
||||
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),
|
||||
input_inject: Subject(Key),
|
||||
exit: Subject(Nil),
|
||||
)
|
||||
}
|
||||
@@ -23,14 +33,14 @@ pub fn new(
|
||||
ui: Subject(ui_control.Control),
|
||||
mpv: Subject(mpv_control.Control),
|
||||
input_keys_name: Name(Key),
|
||||
input_inject_name: Name(Key),
|
||||
exit: Subject(Nil),
|
||||
) -> Result(Nil, String) {
|
||||
let input_keys = process.named_subject(input_keys_name)
|
||||
let input_inject = process.named_subject(input_inject_name)
|
||||
|
||||
let input = Input(False, "")
|
||||
|
||||
case
|
||||
actor.new(State(ui, mpv, input_inject, exit))
|
||||
actor.new(State(Idle, input, ui, mpv, exit))
|
||||
|> actor.on_message(handle_message)
|
||||
|> actor.start
|
||||
{
|
||||
@@ -49,28 +59,53 @@ pub fn new(
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_message(
|
||||
state: State(ui, mpv, input_inject, exit),
|
||||
control: Control,
|
||||
) -> actor.Next(State(ui, mpv, input_inject, exit), Control) {
|
||||
fn handle_message(state: State, control: Control) -> actor.Next(State, Control) {
|
||||
case control {
|
||||
control.Search([]) -> {
|
||||
process.send(state.input_inject, key.Continue([key.input_introducer]))
|
||||
control.Search -> {
|
||||
update_search(state.ui, "searching: ")
|
||||
actor.continue(state)
|
||||
|
||||
actor.continue(
|
||||
State(
|
||||
..state,
|
||||
mode: Searching,
|
||||
input: Input(..state.input, capturing: True),
|
||||
),
|
||||
)
|
||||
}
|
||||
control.Search(content) -> {
|
||||
update_search(state.ui, "searching: " <> string.join(content, ""))
|
||||
actor.continue(state)
|
||||
|
||||
control.Raw(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.Input(_) -> {
|
||||
update_search(state.ui, "")
|
||||
actor.continue(state)
|
||||
control.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 -> {
|
||||
// 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 -> {
|
||||
echo "toggling play/pause"
|
||||
|
||||
process.send(state.mpv, mpv_control.TogglePlayPause)
|
||||
update_playback_time(state.mpv, state.ui)
|
||||
actor.continue(state)
|
||||
@@ -133,24 +168,14 @@ 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. The keys can be either be
|
||||
/// - instances of `Continue` which are "streamed"input, or
|
||||
/// - instances of "final" keys that are the result of "end of input"
|
||||
/// `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 _ = case
|
||||
let _ =
|
||||
process.new_selector()
|
||||
|> process.select(input_keys)
|
||||
|> process.selector_receive_forever
|
||||
{
|
||||
key.Continue(buffer:) ->
|
||||
Ok(process.send(musicplayer, control.Search(buffer)))
|
||||
|
||||
key ->
|
||||
key
|
||||
|> control.from_key
|
||||
|> result.map(process.send(musicplayer, _))
|
||||
}
|
||||
|> control.from_key
|
||||
|> result.map(process.send(musicplayer, _))
|
||||
|
||||
handle_key(musicplayer, input_keys)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user