//// Main application loop: sample /proc, render, handle keys. import etch/command import etch/erlang/input import etch/erlang/tty import etch/event.{ type Event, type KeyCode, Backspace, Char, DownArrow, End, Enter, Esc, Home, Key, PageDown, PageUp, UpArrow, } import etch/stdout import etch/terminal import gleam/dict import gleam/erlang/process import gleam/int import gleam/list import gleam/option.{None, Some} import gleam/string import procfs.{type Snapshot} import ui.{ type SortKey, type ViewState, ConfirmKill, Filter, Help, Normal, SortCommand, SortCpu, SortMem, SortPid, SortTime, } const refresh_ms = 1000 @external(erlang, "erlang", "halt") fn halt(code: Int) -> Nil @external(erlang, "os", "cmd") fn os_cmd(command: String) -> String pub fn run() -> Nil { let #(cols, rows) = window_size() let assert Ok(_) = tty.enter_raw() stdout.execute([ command.EnterAlternateScreen, command.HideCursor, command.Clear(terminal.All), command.DisableLineWrap, command.SetTitle("monitor"), ]) // Seed samples so first painted frame has real CPU % let prev_cores = dict.new() let prev_ticks = dict.new() let snap0 = procfs.sample_with_core_history(0, 0, prev_cores, prev_ticks) process.sleep(200) let snap = procfs.sample_with_core_history( snap0.prev_cpu_total, snap0.prev_cpu_idle, procfs.core_history_from(snap0), snap0.prev_proc_ticks, ) let view = ui.initial_view(cols, rows) |> ui.push_history(snap) loop(snap, view) } fn loop(snap: Snapshot, view: ViewState) -> Nil { let #(cols, rows) = window_size() let view = case cols != view.cols || rows != view.rows { True -> ui.ViewState(..view, cols: cols, rows: rows) False -> view } stdout.execute(ui.render(snap, view)) let #(snap2, view2, quit) = handle_input_window(snap, view, refresh_ms) case quit { True -> cleanup() False -> { let view2 = ui.push_history(view2, snap2) loop(snap2, view2) } } } /// Poll for keys up to `budget_ms`, then refresh system stats. fn handle_input_window( snap: Snapshot, view: ViewState, budget_ms: Int, ) -> #(Snapshot, ViewState, Bool) { let step = 50 handle_input_window_go(snap, view, budget_ms, step) } fn handle_input_window_go( snap: Snapshot, view: ViewState, remaining: Int, step: Int, ) -> #(Snapshot, ViewState, Bool) { case remaining <= 0 { True -> { let snap2 = refresh(snap) #(snap2, view, False) } False -> { case input.poll(0) { Some(Ok(ev)) -> case apply_event(snap, view, ev) { #(snap2, view2, True) -> #(snap2, view2, True) #(snap2, view2, False) -> { // Immediate redraw on keypress for snappy feel stdout.execute(ui.render(snap2, view2)) handle_input_window_go(snap2, view2, remaining - step, step) } } Some(Error(_)) -> { process.sleep(step) handle_input_window_go(snap, view, remaining - step, step) } None -> { process.sleep(step) handle_input_window_go(snap, view, remaining - step, step) } } } } } fn refresh(snap: Snapshot) -> Snapshot { procfs.sample_with_core_history( snap.prev_cpu_total, snap.prev_cpu_idle, procfs.core_history_from(snap), snap.prev_proc_ticks, ) } fn apply_event( snap: Snapshot, view: ViewState, ev: Event, ) -> #(Snapshot, ViewState, Bool) { case ev { Key(ke) -> apply_key(snap, view, ke.code, ke.modifiers.control) _ -> #(snap, view, False) } } fn apply_key( snap: Snapshot, view: ViewState, code: KeyCode, ctrl: Bool, ) -> #(Snapshot, ViewState, Bool) { case view.mode { Help -> #(snap, ui.ViewState(..view, mode: Normal), False) ConfirmKill -> case code { Char("y") | Char("Y") -> { case ui.selected_process(view, snap) { Ok(p) -> { let _ = os_cmd("kill -15 " <> int.to_string(p.pid) <> " 2>/dev/null") Nil } Error(_) -> Nil } #(refresh(snap), ui.ViewState(..view, mode: Normal), False) } Char("n") | Char("N") | Esc -> #( snap, ui.ViewState(..view, mode: Normal), False, ) _ -> #(snap, view, False) } Filter -> case code { Esc -> #( snap, ui.ViewState(..view, mode: Normal, filter: "", selected: 0, scroll: 0), False, ) Enter -> #( snap, ui.ViewState(..view, mode: Normal, selected: 0, scroll: 0), False, ) Backspace -> { let f = string.drop_end(view.filter, 1) #( snap, ui.ViewState(..view, filter: f, selected: 0, scroll: 0), False, ) } Char(c) -> case string.byte_size(c) == 1 && !ctrl && is_printable(c) { True -> #( snap, ui.ViewState( ..view, filter: view.filter <> c, selected: 0, scroll: 0, ), False, ) False -> #(snap, view, False) } _ -> #(snap, view, False) } Normal -> case code { Char("q") | Char("Q") | Esc -> #(snap, view, True) Char("h") | Char("?") -> #( snap, ui.ViewState(..view, mode: Help), False, ) Char("/") -> #(snap, ui.ViewState(..view, mode: Filter), False) Char("k") | Char("K") -> case ui.selected_process(view, snap) { Ok(_) -> #(snap, ui.ViewState(..view, mode: ConfirmKill), False) Error(_) -> #(snap, view, False) } Char("j") | DownArrow -> #(snap, move_sel(view, snap, 1), False) Char("J") -> #(snap, move_sel(view, snap, 10), False) UpArrow | Char("u") -> #(snap, move_sel(view, snap, -1), False) PageDown -> #(snap, move_sel(view, snap, page(view)), False) PageUp -> #(snap, move_sel(view, snap, -page(view)), False) Home | Char("g") -> #( snap, ui.ViewState(..view, selected: 0, scroll: 0), False, ) End | Char("G") -> { let n = list.length(ui.visible_processes(view, snap)) let sel = int.max(0, n - 1) #(snap, ui.ViewState(..view, selected: sel), False) } Char("c") -> #(snap, set_sort(view, SortCpu), False) Char("m") -> #(snap, set_sort(view, SortMem), False) Char("p") -> #(snap, set_sort(view, SortPid), False) Char("t") -> #(snap, set_sort(view, SortTime), False) Char("n") -> #(snap, set_sort(view, SortCommand), False) Char("r") -> #( snap, ui.ViewState(..view, reverse: !view.reverse, selected: 0, scroll: 0), False, ) Char(" ") -> #(refresh(snap), view, False) _ -> #(snap, view, False) } } } fn is_printable(c: String) -> Bool { case string.to_utf_codepoints(c) { [cp] -> { let n = string.utf_codepoint_to_int(cp) n >= 32 && n <= 126 } _ -> False } } fn set_sort(view: ViewState, key: SortKey) -> ViewState { case view.sort == key { True -> ui.ViewState(..view, reverse: !view.reverse, selected: 0, scroll: 0) False -> ui.ViewState( ..view, sort: key, reverse: case key { SortCpu | SortMem | SortTime -> True SortPid | SortCommand -> False }, selected: 0, scroll: 0, ) } } fn move_sel(view: ViewState, snap: Snapshot, delta: Int) -> ViewState { let n = list.length(ui.visible_processes(view, snap)) let sel = int.clamp(view.selected + delta, 0, int.max(0, n - 1)) ui.ViewState(..view, selected: sel) } fn page(view: ViewState) -> Int { int.max(5, view.rows / 2) } fn window_size() -> #(Int, Int) { case tty.window_size() { Ok(#(c, r)) -> #(c, r) Error(_) -> #(100, 30) } } fn cleanup() -> Nil { stdout.execute([ command.EnableLineWrap, command.ShowCursor, command.Clear(terminal.All), command.LeaveAlternateScreen, ]) let _ = tty.exit_raw() halt(0) }