Sign in

    moui_terminal_host

    Experimental terminal host: raw-mode TTY input channels for the MoUI TUI renderer

    moui
    tui
    terminal
    raw-mode
    experimental
    Download zip
    Author
    Version
    0.2.1
    License
    Apache-2.0
    Last updated
    9 hours ago
    Downloads
    1

    wzzc-dev/moui_terminal_host does not have a README file

    CellPosition

    pub(all) struct CellPosition {
    col : Int
    row : Int
    } derive(Eq,
    Debug
    )

    终端报告的位置:cell 坐标,0 起算(SGR 的 1 起算在解码时已折算)。

    CellPosition::equal

    CellPosition::not_equal

    fn CellPosition::not_equal(x : CellPosition, y : CellPosition) -> Bool

    MousePhase

    pub(all) enum MousePhase {
    Press
    Release
    Drag
    } derive(Eq,
    Debug
    )

    MousePhase::equal

    fn MousePhase::equal(MousePhase, MousePhase) -> Bool

    MousePhase::not_equal

    fn MousePhase::not_equal(x : MousePhase, y : MousePhase) -> Bool

    MouseReport

    pub(all) struct MouseReport {
    position : CellPosition
    button : Int
    phase : MousePhase
    modifiers :
    KeyModifiers

    } derive(Eq,
    Debug
    )

    SGR(1006) 鼠标报告。button 为终端原始按钮号:0 左 1 中 2 右, 3 表示「无键移动」(1003 模式),不是第 4 个按钮。

    MouseReport::equal

    fn MouseReport::equal(MouseReport, MouseReport) -> Bool

    MouseReport::not_equal

    fn MouseReport::not_equal(x : MouseReport, y : MouseReport) -> Bool

    PollState

    pub enum PollState {
    Ready
    Timeout
    Unavailable
    } derive(Eq,
    Debug
    )

    wait 的结果。Timeout 不是错误:它就是「这一轮没有更多字节」的事实, 解码器据此把裸 ESC 结算成 Escape 键。

    PollState::equal

    fn PollState::equal(PollState, PollState) -> Bool

    PollState::not_equal

    fn PollState::not_equal(x : PollState, y : PollState) -> Bool

    TerminalDecoder

    pub struct TerminalDecoder {
    pending : Array[Int]
    }

    TerminalDecoder::feed_bytes

    fn TerminalDecoder::feed_bytes(self : TerminalDecoder, chunk : Bytes) -> Array[TerminalEvent]

    一批原始字节进来:能拼出几条完整序列就出几条事件,拼不出的留在缓冲里。

    TerminalDecoder::flush

    数据流停了(poll 超时)时调用:把只能靠超时消歧的事实结算掉。

    单独一个 ESC 既可能是 Escape 键本身,也可能是 ESC x / ESC[A 的前缀。 读到一半就派发会把 Alt-方向键拆成两次事件;死等又会让 Escape 永不响应。 所以解码时把裸 ESC 留在缓冲,由宿主在「这一轮 poll 没再来字节」时 flush—— 瞬时响应与消歧都保住,代价只是 Escape 键晚一个 poll 周期。

    此时缓冲里若还剩别的前缀(例如终端只发出 ESC [),它已经拼不成任何合法 序列:整条丢弃。留着它只会让下一次按键与残留前缀拼出错误事件。

    TerminalDecoder::new

    TerminalDecoder::pending_bytes

    fn TerminalDecoder::pending_bytes(self : TerminalDecoder) -> Int

    尚未拼成完整序列的字节数,诊断与测试用。

    TerminalEvent

    一条完整序列解码出的输入事实。键盘/鼠标/滚轮三类都在此暴露, backend 不吞掉任何一类。

    TerminalEvent::equal

    TerminalEvent::not_equal

    fn TerminalEvent::not_equal(x : TerminalEvent, y : TerminalEvent) -> Bool

    TerminalExit

    pub(all) enum TerminalExit {
    Closed
    NotTerminal
    NoGridSize
    SetupFailed(String)
    RendererRejected(String)
    WriteFailed(String)
    } derive(Eq,
    Debug
    )

    宿主退出原因。正常退出只有一种:应用自己请求关闭。

    TerminalExit::describe

    fn TerminalExit::describe(self : TerminalExit) -> String

    给用户看的一句话。必须在离开 alt screen 之后打印,所以交回调用方输出。

    TerminalExit::equal

    TerminalExit::not_equal

    fn TerminalExit::not_equal(x : TerminalExit, y : TerminalExit) -> Bool

    TerminalInput

    三条输入通道:终端事实 → 宿主事件,一条都不吞。

    分工按「谁可能知道这个事实」切开:
    • decode 只报终端能表达的:cell 坐标、滚轮一格、原始按钮号、修饰键位;
    • 这里只补终端不知道的:cell→逻辑像素、按钮编号对照、宿主事件外壳;
    • 派发时机在 terminal_host:每条事实立刻走一次 update→present, 所以「按下即响应」不依赖 Enter 或提交。

    键名与文本分两条事件,和桌面端「按键 + IME 提交」同形:键盘通道只报哪个键、 什么状态,插入只由文本通道报。两边都带文本会让焦点文本框把同一个字符插两遍 (它读 event.text,见 moui/views/text/text_input_event_handlers.mbt:234)。

    TerminalInput::metrics

    当前视口度量:宿主用它建 HostSurface,并在 resize 后交给渲染会话重排。

    TerminalInput::new

    fn TerminalInput::new(cols~ : Int, rows~ : Int, cell? :
    CellBox
    ) -> TerminalInput

    建立输入通道。cols/rows 来自 Tty::grid,所以构造即已知视口尺寸; 宿主必须先用 metrics 造 HostSurface,再去绑渲染器——尺寸为 0 的 surface 会被共享的 bind gate 拒绝。

    TerminalInput::point_of

    cell 坐标 → 逻辑像素,取格心。取左上角会让「正好落在格子边界」的盒差一格; 格心恒在自己量化盒内部,末行末列也不会越出视口。

    TerminalInput::resize

    fn TerminalInput::resize(self : TerminalInput, cols : Int, rows : Int) ->
    Event

    尺寸通道:一次 resize 产出一条新的视口事实。宿主拿到它之后还要做两件事 ——让渲染会话按新度量重画,让运行时走完整的 update→layout→present。

    TerminalInput::to_host

    一条终端事实换算成宿主事件。None 只发生在「这条事实不该进入应用」: 按钮号与相位矛盾(无键却报按下/释放),或能力表关掉了某条通道。

    Tty

    pub struct Tty {
    in_fd : Int
    out_fd : Int
    }

    控制终端:输入固定在 fd 0,输出固定在 fd 1(终端会话的约定)。

    两个 fd 分开存,是因为「stdin 是管道」(测试里常见)和「stdout 被重定向到 文件」是两种不同的失败,合在一起报会让用户不知道该改哪一边。

    Tty::current

    fn Tty::current() -> Tty

    当前进程的控制终端。这里不检查它是不是 tty:调用方先用 is_terminal 判断,才能在非 tty 环境(管道、CI)里给出人话错误而不是 raw mode 失败码。

    Tty::emit

    fn Tty::emit(self : Tty, bytes : Bytes) -> String?

    把字节写到终端。短写在 C 侧循环补全;写不进就返回原因, 让宿主有机会先还原终端再退出。

    Tty::enter_raw

    fn Tty::enter_raw(self : Tty) -> String?

    保存终端设置并进入 raw mode。进入后必须成对调用 restore, 否则 shell 会被留在 raw mode。

    失败用返回值而不是 raise:Unit raise 的错误类型是擦除的,宿主拿回的 是一个 Error 而不是这里写的那句话,而终端宿主必须把 raw mode 失败、信号 安装失败、写失败分别映射成 TerminalExit 的不同格子。返回值也让 TuiSink::write(要求纯函数)那条出口路径不用 try/catch。

    Tty::grid

    fn Tty::grid(self : Tty) -> (Int, Int)?

    终端网格尺寸(列, 行)。非 tty、或尺寸报 0 时返回 None。

    Tty::install_signal_handlers

    fn Tty::install_signal_handlers(_self : Tty) -> String?

    装 SIGWINCH(resize 标志)与 SIGINT/SIGTERM/SIGHUP(退出标志)。 两类信号都只置位,处置权留在主循环:协议字节只能在正常退出路径上关闭。

    Tty::is_terminal

    fn Tty::is_terminal(self : Tty) -> Bool

    Tty::restore

    fn Tty::restore(self : Tty) -> Unit

    还原进入 raw mode 之前的终端设置。没进过 raw mode 时是空操作, 因此可以放在清理路径上无条件调用。

    Tty::take_input

    fn Tty::take_input(self : Tty) -> Bytes

    取走当前可读字节。空 Bytes 表示没有数据,或对端已关闭(EOF)。

    Tty::take_resize_signal

    fn Tty::take_resize_signal(_self : Tty) -> Bool

    读取并清零 resize 信号:每次只报一次,宿主据此把 resize 当成一次完整重排。

    Tty::terminate_requested

    fn Tty::terminate_requested(_self : Tty) -> Bool

    是否收到过退出信号。不清零:标志一旦置位,主循环每一轮都要继续看到它, 直到走完还原。raw mode 有意保留 ISIG,所以 Ctrl-C 到不了按键通道, 只能从这条路结算。

    Tty::wait

    fn Tty::wait(self : Tty, timeout_ms~ : Int) -> PollState

    等待输入,最多 timeout_ms 毫秒。SIGWINCH 会打断等待并返回 Timeout, 让宿主立刻去查新尺寸,而不是等下一个超时。

    Tty::write_text

    fn Tty::write_text(self : Tty, text : String) -> String?

    把一段文本按 UTF-8 写到终端。协议字节和投影输出都走这一条,编码只在此处 发生:String::to_bytes 已被标 deprecated,而 CJK 内容必须按 UTF-8 出去。

    WheelReport

    pub(all) struct WheelReport {
    position : CellPosition
    up : Bool
    modifiers :
    KeyModifiers

    } derive(Eq,
    Debug
    )

    滚轮:终端只给「一格」这个事实,不给像素增量。换算成 @core.PointerEvent.wheel_delta 由 input_channel 按平台同款行高策略完成。

    WheelReport::equal

    fn WheelReport::equal(WheelReport, WheelReport) -> Bool

    WheelReport::not_equal

    fn WheelReport::not_equal(x : WheelReport, y : WheelReport) -> Bool

    run_program

    fn[Model, Msg] run_program(program~ :
    Program
    [Model, Msg], cell? :
    CellBox
    , idle_timeout_ms? : Int, theme? :
    Theme
    ?) -> TerminalExit

    跑一个 Program:进入 alt screen 与 raw mode,直到应用请求关闭或终端不可用。 返回前终端一定已经还原。

    渲染器在此显式声明为 tui-ansi,不走多渲染器择优:终端没有 GPU surface 可回退,择优失败只会把原因藏起来。

    run_view

    fn[Msg] run_view(root~ :
    View
    [Msg], cell? :
    CellBox
    , idle_timeout_ms? : Int, on_message? : (Msg) -> Unit?, theme? :
    Theme
    ?) -> TerminalExit

    跑一个根 View,给不需要 Program 的薄入口用。