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 {
|
pub fn main() -> Nil {
|
||||||
let input_keys_name: Name(Key) = process.new_name("input_keys")
|
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(ui) = ui.new()
|
||||||
let assert Ok(mpv) = mpv.new()
|
let assert Ok(mpv) = mpv.new()
|
||||||
|
|
||||||
let exit = process.new_subject()
|
let exit = process.new_subject()
|
||||||
let assert Ok(_) =
|
let assert Ok(_) = musicplayer.new(ui, mpv, input_keys_name, exit)
|
||||||
musicplayer.new(ui, mpv, input_keys_name, input_inject_name, exit)
|
|
||||||
process.receive_forever(exit)
|
process.receive_forever(exit)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,26 +2,29 @@ import musicplayer/input/key.{type Key}
|
|||||||
|
|
||||||
pub type Control {
|
pub type Control {
|
||||||
TogglePlayPause
|
TogglePlayPause
|
||||||
|
Search
|
||||||
|
|
||||||
Input(String)
|
Raw(String)
|
||||||
Search(List(String))
|
Return
|
||||||
|
Backspace
|
||||||
|
|
||||||
Exit
|
Exit
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_key(key: Key) -> Result(Control, Nil) {
|
pub fn from_key(key: Key) -> Result(Control, Nil) {
|
||||||
case key {
|
case key {
|
||||||
key.Input(content) -> Ok(Input(content))
|
key.Return -> Ok(Return)
|
||||||
key.Char(char) -> char_control(char)
|
key.Backspace -> Ok(Backspace)
|
||||||
|
key.Char(char) -> Ok(char_control(char))
|
||||||
_ -> Error(Nil)
|
_ -> Error(Nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn char_control(char: String) -> Result(Control, Nil) {
|
fn char_control(char: String) -> Control {
|
||||||
case char {
|
case char {
|
||||||
" " -> Ok(TogglePlayPause)
|
" " -> TogglePlayPause
|
||||||
"/" -> Ok(Search([]))
|
"/" -> Search
|
||||||
"q" -> Ok(Exit)
|
"q" -> Exit
|
||||||
_ -> Error(Nil)
|
_ -> Raw(char)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,12 @@ import gleam/erlang/process.{type Name}
|
|||||||
|
|
||||||
import musicplayer/input/key.{type Key}
|
import musicplayer/input/key.{type Key}
|
||||||
|
|
||||||
/// `new` accepts two named subjects:
|
/// `new` accepts a subject that all input will be sent to
|
||||||
/// - one to send the input to, and
|
pub fn new(input_keys_name: Name(Key)) -> Nil {
|
||||||
/// - 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 {
|
|
||||||
let _ =
|
let _ =
|
||||||
process.spawn(fn() {
|
process.spawn(fn() {
|
||||||
let input_keys = process.named_subject(input_keys_name)
|
let input_keys = process.named_subject(input_keys_name)
|
||||||
let input_inject = process.named_subject(input_inject_name)
|
key.read_input_until_key(input_keys)
|
||||||
|
|
||||||
let assert Ok(_) = process.register(process.self(), input_inject_name)
|
|
||||||
|
|
||||||
key.read_input_until_key(input_keys, input_inject)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
key.start_raw_shell()
|
key.start_raw_shell()
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import gleam/erlang/atom
|
import gleam/erlang/atom
|
||||||
import gleam/erlang/process.{type Subject}
|
import gleam/erlang/process.{type Subject}
|
||||||
import gleam/list
|
import gleam/list
|
||||||
import gleam/string
|
|
||||||
|
|
||||||
import musicplayer/input/internal as internal_input
|
import musicplayer/input/internal as internal_input
|
||||||
|
|
||||||
pub type Key {
|
pub type Key {
|
||||||
|
Continue(buffer: List(String))
|
||||||
Char(String)
|
Char(String)
|
||||||
|
|
||||||
Input(String)
|
|
||||||
Left
|
Left
|
||||||
Right
|
Right
|
||||||
Up
|
Up
|
||||||
Down
|
Down
|
||||||
|
|
||||||
Continue(buffer: List(String))
|
Return
|
||||||
|
Backspace
|
||||||
|
|
||||||
Unknown
|
Unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,8 +24,9 @@ pub const esc = "\u{001B}"
|
|||||||
// control sequence introducer
|
// control sequence introducer
|
||||||
pub const csi = "["
|
pub const csi = "["
|
||||||
|
|
||||||
// input introducer
|
pub const return = "\r"
|
||||||
pub const input_introducer = "::"
|
|
||||||
|
pub const backspace = "\u{007F}"
|
||||||
|
|
||||||
pub fn from_list(l: List(String)) -> Key {
|
pub fn from_list(l: List(String)) -> Key {
|
||||||
case l {
|
case l {
|
||||||
@@ -35,18 +37,9 @@ pub fn from_list(l: List(String)) -> Key {
|
|||||||
|
|
||||||
[e, c] if e == esc && c == csi -> Continue(l)
|
[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)
|
[e] if e == esc -> Continue(l)
|
||||||
|
[bs] if bs == backspace -> Backspace
|
||||||
|
[rtn] if rtn == return -> Return
|
||||||
[char] -> Char(char)
|
[char] -> Char(char)
|
||||||
|
|
||||||
[] -> Continue([])
|
[] -> Continue([])
|
||||||
@@ -60,35 +53,24 @@ pub fn start_raw_shell() {
|
|||||||
internal_input.shell_start_interactive(#(no_shell, raw))
|
internal_input.shell_start_interactive(#(no_shell, raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_input_until_key(
|
pub fn read_input_until_key(input_keys: Subject(Key)) -> Nil {
|
||||||
input_keys: Subject(Key),
|
read_input_until_key_loop([], input_keys)
|
||||||
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 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
|
case
|
||||||
internal_input.read_input()
|
internal_input.read_input()
|
||||||
|> list.wrap
|
|> list.wrap
|
||||||
|> list.append(l, _)
|
|> list.append(l, _)
|
||||||
|> from_list
|
|> from_list
|
||||||
{
|
{
|
||||||
Continue(l) -> {
|
Continue(l) -> read_input_until_key_loop(l, input_keys)
|
||||||
case l {
|
k -> {
|
||||||
[ii, cmd] if ii == input_introducer -> {
|
process.send(input_keys, k)
|
||||||
process.send(input_keys, Continue([cmd]))
|
read_input_until_key_loop([], input_keys)
|
||||||
forward_input_to(l, input_keys)
|
|
||||||
}
|
}
|
||||||
_ -> forward_input_to(l, input_keys)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
k -> process.send(input_keys, k)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,21 @@ import musicplayer/mpv/control as mpv_control
|
|||||||
import musicplayer/ui/control as ui_control
|
import musicplayer/ui/control as ui_control
|
||||||
import musicplayer/ui/layout
|
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(
|
State(
|
||||||
|
mode: Mode,
|
||||||
|
input: Input,
|
||||||
ui: Subject(ui_control.Control),
|
ui: Subject(ui_control.Control),
|
||||||
mpv: Subject(mpv_control.Control),
|
mpv: Subject(mpv_control.Control),
|
||||||
input_inject: Subject(Key),
|
|
||||||
exit: Subject(Nil),
|
exit: Subject(Nil),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -23,14 +33,14 @@ pub fn new(
|
|||||||
ui: Subject(ui_control.Control),
|
ui: Subject(ui_control.Control),
|
||||||
mpv: Subject(mpv_control.Control),
|
mpv: Subject(mpv_control.Control),
|
||||||
input_keys_name: Name(Key),
|
input_keys_name: Name(Key),
|
||||||
input_inject_name: Name(Key),
|
|
||||||
exit: Subject(Nil),
|
exit: Subject(Nil),
|
||||||
) -> Result(Nil, String) {
|
) -> Result(Nil, String) {
|
||||||
let input_keys = process.named_subject(input_keys_name)
|
let input_keys = process.named_subject(input_keys_name)
|
||||||
let input_inject = process.named_subject(input_inject_name)
|
|
||||||
|
let input = Input(False, "")
|
||||||
|
|
||||||
case
|
case
|
||||||
actor.new(State(ui, mpv, input_inject, exit))
|
actor.new(State(Idle, input, ui, mpv, exit))
|
||||||
|> actor.on_message(handle_message)
|
|> actor.on_message(handle_message)
|
||||||
|> actor.start
|
|> actor.start
|
||||||
{
|
{
|
||||||
@@ -49,28 +59,53 @@ pub fn new(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_message(
|
fn handle_message(state: State, control: Control) -> actor.Next(State, Control) {
|
||||||
state: State(ui, mpv, input_inject, exit),
|
|
||||||
control: Control,
|
|
||||||
) -> actor.Next(State(ui, mpv, input_inject, exit), Control) {
|
|
||||||
case control {
|
case control {
|
||||||
control.Search([]) -> {
|
control.Search -> {
|
||||||
process.send(state.input_inject, key.Continue([key.input_introducer]))
|
|
||||||
update_search(state.ui, "searching: ")
|
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, ""))
|
control.Raw(content) -> {
|
||||||
actor.continue(state)
|
let content = case state.mode {
|
||||||
|
Idle -> state.input.content
|
||||||
|
Searching -> {
|
||||||
|
let updated = state.input.content <> content
|
||||||
|
update_search(state.ui, "searching: " <> updated)
|
||||||
|
updated
|
||||||
}
|
}
|
||||||
control.Input(_) -> {
|
}
|
||||||
update_search(state.ui, "")
|
|
||||||
actor.continue(state)
|
actor.continue(State(..state, input: Input(..state.input, content:)))
|
||||||
|
}
|
||||||
|
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 -> {
|
control.TogglePlayPause -> {
|
||||||
echo "toggling play/pause"
|
|
||||||
|
|
||||||
process.send(state.mpv, mpv_control.TogglePlayPause)
|
process.send(state.mpv, mpv_control.TogglePlayPause)
|
||||||
update_playback_time(state.mpv, state.ui)
|
update_playback_time(state.mpv, state.ui)
|
||||||
actor.continue(state)
|
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))
|
process.send(ui, ui_control.UpdateState(layout.Search, content))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `handle_key` listens to a subject onto which `input` will send messages with
|
/// `handle_key` listens to a subject onto which `input` will send messages with `Key`s
|
||||||
/// `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"
|
|
||||||
fn handle_key(musicplayer: Subject(Control), input_keys: Subject(Key)) -> Nil {
|
fn handle_key(musicplayer: Subject(Control), input_keys: Subject(Key)) -> Nil {
|
||||||
let _ = case
|
let _ =
|
||||||
process.new_selector()
|
process.new_selector()
|
||||||
|> process.select(input_keys)
|
|> process.select(input_keys)
|
||||||
|> process.selector_receive_forever
|
|> process.selector_receive_forever
|
||||||
{
|
|
||||||
key.Continue(buffer:) ->
|
|
||||||
Ok(process.send(musicplayer, control.Search(buffer)))
|
|
||||||
|
|
||||||
key ->
|
|
||||||
key
|
|
||||||
|> control.from_key
|
|> control.from_key
|
||||||
|> result.map(process.send(musicplayer, _))
|
|> result.map(process.send(musicplayer, _))
|
||||||
}
|
|
||||||
|
|
||||||
handle_key(musicplayer, input_keys)
|
handle_key(musicplayer, input_keys)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import gleam/list
|
import gleam/list
|
||||||
import gleeunit
|
import gleeunit
|
||||||
|
|
||||||
import musicplayer/input/key.{type Key, Char, csi, esc, input_introducer as ii}
|
import musicplayer/input/key.{type Key, Char, backspace, csi, esc, return}
|
||||||
|
|
||||||
pub fn main() -> Nil {
|
pub fn main() -> Nil {
|
||||||
gleeunit.main()
|
gleeunit.main()
|
||||||
@@ -27,11 +27,8 @@ pub fn key_from_list_test() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
let input_tests = [
|
let input_tests = [
|
||||||
TestCase([ii], key.Continue([ii])),
|
TestCase([return], key.Return),
|
||||||
TestCase([ii, "a"], key.Continue([ii, "a"])),
|
TestCase([backspace], key.Backspace),
|
||||||
TestCase([ii, "a", "b"], key.Continue([ii, "ab"])),
|
|
||||||
TestCase([ii, "ab", "\u{007F}"], key.Continue([ii, "a"])),
|
|
||||||
TestCase([ii, "ab", "\r"], key.Input("ab")),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
let test_cases = [base_tests, char_tests, escape_tests, input_tests]
|
let test_cases = [base_tests, char_tests, escape_tests, input_tests]
|
||||||
|
|||||||
Reference in New Issue
Block a user