Current section
Files
Jump to
Current section
Files
src/etch/erlang/tty.gleam
import etch/event.{type Event, type EventError, parse_events}
import gleam/erlang/process.{type Pid}
import gleam/string
pub type TerminalError {
CouldNotGetWindowSize
FailedToEnterRawMode
FailedToExitRawMode
}
@external(erlang, "terminal_ffi", "enter_raw")
fn enter_raw_ffi() -> Result(Nil, TerminalError)
@external(erlang, "terminal_ffi", "exit_raw")
fn exit_raw_ffi() -> Result(Nil, TerminalError)
@external(erlang, "tty_state", "init")
@internal
pub fn tty_state_init() -> Nil
@external(erlang, "tty_state", "set_raw")
fn set_raw(is_raw: Bool) -> Nil
@external(erlang, "tty_state", "is_raw_mode")
pub fn is_raw_mode() -> Bool
/// Returns current window size.
@external(erlang, "terminal_ffi", "window_size")
pub fn window_size() -> Result(#(Int, Int), TerminalError)
/// Enters raw mode.
///
/// Raw mode is a mode where the terminal does not process input, but instead
/// passes it directly to the application. This means that:
/// - Input is not echoed to the screen
/// - Input is not line-buffered (characters are available immediately)
/// - Some special characters are not processed by the terminal
///
/// This is necessary for terminal UI applications that need to handle
/// keyboard input and mouse events directly.
pub fn enter_raw() -> Result(Nil, TerminalError) {
set_raw(True)
case enter_raw_ffi() {
Error(e) -> Error(e)
Ok(_) -> {
let input_loop_pid = get_input_loop_pid()
case input_loop_pid {
Ok(pid) -> {
process.send_exit(pid)
start_input_loop()
Ok(Nil)
}
Error(_) -> Error(FailedToEnterRawMode)
}
}
}
}
/// Exits raw mode.
///
/// Raw mode is a mode where the terminal does not process input, but instead
/// passes it directly to the application. This means that:
/// - Input is not echoed to the screen
/// - Input is not line-buffered (characters are available immediately)
/// - Some special characters are not processed by the terminal
pub fn exit_raw() -> Result(Nil, TerminalError) {
set_raw(False)
exit_raw_ffi()
}
// it's kinda stupid, but i have to move internal input code to this file.
// reason: to correctly enter raw mode. because erlang_read_line blocks the thread,
// after entering raw mode you need to press enter once more after calling enter_raw()
// (see input_loop()). so i decided to just kill the input_loop() process and restart
// it. for that, i need to know and store its pid. but i can't restart input_loop()
// from tty.gleam without importing from input.gleam, which already imports from
// tty.gleam, and gleam doesn't allow circular dependencies.
@external(erlang, "tty_state", "save_input_loop_pid")
fn save_input_loop_pid(pid: Pid) -> Nil
@external(erlang, "tty_state", "get_input_loop_pid")
fn get_input_loop_pid() -> Result(Pid, Nil)
@external(erlang, "io", "get_chars")
fn get_chars(prompt: String, n: Int) -> String
@external(erlang, "io", "get_line")
fn erlang_read(prompt: String) -> String
@external(erlang, "input_ffi", "push")
fn push(event: Result(Event, EventError)) -> Nil
@internal
pub fn start_input_loop() {
let input_loop_pid = process.spawn(fn() { input_loop() })
save_input_loop_pid(input_loop_pid)
}
fn input_loop() {
let str = case is_raw_mode() {
True -> get_chars("", 128)
False -> erlang_read("")
}
let str = string.to_graphemes(str)
let events = parse_events(str, "", [], False)
push_events(events)
input_loop()
}
fn push_events(events: List(Result(Event, EventError))) {
case events {
[] -> Nil
[e, ..rest] -> {
push(e)
push_events(rest)
}
}
}