///|
/// Structured render payload for `render_frame_state` clients. The server
/// diffs terminal state against what each client already holds, so `lines`
/// carries only the rows to repaint unless `full` is set. Runs keep explicit
/// `col` and `width` so the client can map text back to grid cells for
/// selection without replaying a terminal emulator.
pub(all) struct Frame {
/// Whether `lines` covers every viewport row. A diff frame repaints only
/// the listed rows; an empty `runs` array clears its row.
full : Bool
/// Frame-scoped style dictionary referenced by `Run::style`.
styles : Array[RunStyle]
lines : Array[Line]
/// Absent while the client views scrollback or the cursor is hidden.
cursor : Cursor?
} derive(Eq, Debug)
///|
pub(all) struct Line {
y : Int
runs : Array[Run]
} derive(Eq, Debug)
///|
/// One styled run of cells. An ASCII run holds one cell per character, so
/// `width` equals the character count. A non-ASCII glyph always forms its own
/// run whose `width` (1 or 2) is its grid-cell span; the client renders it as
/// a fixed-width centered span so glyph advances cannot push later columns
/// off the grid.
pub(all) struct Run {
col : Int
width : Int
text : String
/// Index into `Frame::styles`; `None` is the default style.
style : Int?
} derive(Eq, Debug)
///|
const RunBold : Int = 0x1
///|
const RunFaint : Int = 0x2
///|
const RunItalic : Int = 0x4
///|
const RunStrikethrough : Int = 0x8
///|
const RunOverline : Int = 0x10
///|
const RunBlink : Int = 0x20
///|
const RunInverse : Int = 0x40
///|
const RunUnderlineShift : Int = 7
///|
/// SGR 8: the run's glyphs ship for selection and copy but must not paint,
/// including their decorations. Bits 7-9 hold the underline kind.
const RunConceal : Int = 0x400
///|
/// Colors are `#rrggbb`; `None` means the client's default foreground or
/// background. The server resolves palette indices (including bold
/// promotion) but never substitutes its own default colors, so client themes
/// keep owning the defaults — including under `inverse`, which the client
/// applies by swapping resolved colors with its theme defaults.
pub(all) struct RunStyle {
fg : String?
bg : String?
underline_color : String?
flags : Int
} derive(Eq, Debug)
///|
pub(all) enum Underline {
None
Single
Double
Curly
Dotted
Dashed
} derive(Eq, Debug)
///|
fn Underline::to_bits(self : Underline) -> Int {
match self {
None => 0
Single => 1
Double => 2
Curly => 3
Dotted => 4
Dashed => 5
}
}
///|
fn Underline::from_bits(bits : Int) -> Underline {
match bits {
1 => Single
2 => Double
3 => Curly
4 => Dotted
5 => Dashed
_ => None
}
}
///|
pub fn RunStyle::new(
fg? : String,
bg? : String,
underline_color? : String,
bold? : Bool = false,
faint? : Bool = false,
italic? : Bool = false,
strikethrough? : Bool = false,
overline? : Bool = false,
blink? : Bool = false,
inverse? : Bool = false,
conceal? : Bool = false,
underline? : Underline = Underline::None,
) -> RunStyle {
let mut flags = underline.to_bits() << RunUnderlineShift
if bold {
flags = flags | RunBold
}
if faint {
flags = flags | RunFaint
}
if italic {
flags = flags | RunItalic
}
if strikethrough {
flags = flags | RunStrikethrough
}
if overline {
flags = flags | RunOverline
}
if blink {
flags = flags | RunBlink
}
if inverse {
flags = flags | RunInverse
}
if conceal {
flags = flags | RunConceal
}
{ fg, bg, underline_color, flags }
}
///|
pub fn RunStyle::bold(self : RunStyle) -> Bool {
(self.flags & RunBold) != 0
}
///|
pub fn RunStyle::faint(self : RunStyle) -> Bool {
(self.flags & RunFaint) != 0
}
///|
pub fn RunStyle::italic(self : RunStyle) -> Bool {
(self.flags & RunItalic) != 0
}
///|
pub fn RunStyle::strikethrough(self : RunStyle) -> Bool {
(self.flags & RunStrikethrough) != 0
}
///|
pub fn RunStyle::overline(self : RunStyle) -> Bool {
(self.flags & RunOverline) != 0
}
///|
pub fn RunStyle::blink(self : RunStyle) -> Bool {
(self.flags & RunBlink) != 0
}
///|
pub fn RunStyle::inverse(self : RunStyle) -> Bool {
(self.flags & RunInverse) != 0
}
///|
pub fn RunStyle::conceal(self : RunStyle) -> Bool {
(self.flags & RunConceal) != 0
}
///|
pub fn RunStyle::underline(self : RunStyle) -> Underline {
Underline::from_bits((self.flags >> RunUnderlineShift) & 7)
}
///|
/// Whether this is the default style a `Run` encodes as `style : None`.
pub fn RunStyle::is_default(self : RunStyle) -> Bool {
self.fg is None &&
self.bg is None &&
self.underline_color is None &&
self.flags == 0
}
///|
pub(all) enum CursorShape {
Block
Bar
Underline
} derive(Eq, Debug)
///|
/// The visible cursor. Coordinates are zero-based viewport cells; a cursor
/// on a wide glyph's trailing cell is already snapped to the glyph head.
/// `color` is set only when the child application overrides the cursor
/// color, so the client theme keeps the default. The glyph under a block
/// cursor, its cell span, and its inverted color are not on the wire: the
/// client derives them from the run grid it already holds.
pub(all) struct Cursor {
x : Int
y : Int
shape : CursorShape
blink : Bool
color : String?
} derive(Eq, Debug)
///|
pub impl ToJson for RunStyle with fn to_json(self) {
let obj : Map[String, Json] = Map([])
if self.fg is Some(value) {
obj["fg"] = Json::string(value)
}
if self.bg is Some(value) {
obj["bg"] = Json::string(value)
}
if self.underline_color is Some(value) {
obj["uc"] = Json::string(value)
}
if self.flags != 0 {
obj["fl"] = self.flags.to_json()
}
Json::object(obj)
}
///|
pub impl @json.FromJson for RunStyle with fn from_json(json, path) {
guard json is Object(fields) else {
raise JsonDecodeError((path, "expected run style object"))
}
let fg = if fields.get("fg") is Some(String(value)) {
Some(value)
} else {
None
}
let bg = if fields.get("bg") is Some(String(value)) {
Some(value)
} else {
None
}
let underline_color = if fields.get("uc") is Some(String(value)) {
Some(value)
} else {
None
}
let flags = if fields.get("fl") is Some(Number(value, ..)) {
value.to_int()
} else {
0
}
{ fg, bg, underline_color, flags }
}
///|
pub impl ToJson for Run with fn to_json(self) {
let obj : Map[String, Json] = {
"c": self.col.to_json(),
"w": self.width.to_json(),
"t": Json::string(self.text),
}
if self.style is Some(index) {
obj["s"] = index.to_json()
}
Json::object(obj)
}
///|
pub impl @json.FromJson for Run with fn from_json(json, path) {
guard json is Object(fields) else {
raise JsonDecodeError((path, "expected run object"))
}
let col = if fields.get("c") is Some(Number(value, ..)) {
value.to_int()
} else {
0
}
let width = if fields.get("w") is Some(Number(value, ..)) {
value.to_int()
} else {
0
}
let text = if fields.get("t") is Some(String(value)) { value } else { "" }
let style = if fields.get("s") is Some(Number(value, ..)) {
Some(value.to_int())
} else {
None
}
{ col, width, text, style }
}
///|
pub impl ToJson for Line with fn to_json(self) {
{ "y": self.y.to_json(), "runs": self.runs.to_json() }
}
///|
pub impl @json.FromJson for Line with fn from_json(json, path) {
guard json is Object(fields) else {
raise JsonDecodeError((path, "expected line object"))
}
let y = if fields.get("y") is Some(Number(value, ..)) {
value.to_int()
} else {
0
}
let runs : Array[Run] = match fields.get("runs") {
Some(runs_json) => @json.from_json(runs_json, path~)
_ => []
}
{ y, runs }
}
///|
fn CursorShape::to_wire(self : CursorShape) -> String {
match self {
Block => "block"
Bar => "bar"
Underline => "underline"
}
}
///|
fn CursorShape::from_wire(value : String) -> CursorShape {
match value {
"bar" => Bar
"underline" => Underline
_ => Block
}
}
///|
pub impl ToJson for Cursor with fn to_json(self) {
let obj : Map[String, Json] = {
"x": self.x.to_json(),
"y": self.y.to_json(),
"shape": Json::string(self.shape.to_wire()),
}
if self.blink {
obj["blink"] = self.blink.to_json()
}
if self.color is Some(value) {
obj["color"] = Json::string(value)
}
Json::object(obj)
}
///|
pub impl @json.FromJson for Cursor with fn from_json(json, path) {
guard json is Object(fields) else {
raise JsonDecodeError((path, "expected cursor object"))
}
let x = if fields.get("x") is Some(Number(value, ..)) {
value.to_int()
} else {
0
}
let y = if fields.get("y") is Some(Number(value, ..)) {
value.to_int()
} else {
0
}
let shape = if fields.get("shape") is Some(String(value)) {
CursorShape::from_wire(value)
} else {
Block
}
let blink = if fields.get("blink") is Some(True) { true } else { false }
let color = if fields.get("color") is Some(String(value)) {
Some(value)
} else {
None
}
{ x, y, shape, blink, color }
}