# SPDX-License-Identifier: MIT # Copyright (c) 2026 code@svcomplex.ai # svw - dependency-free Tcl client for the svw.ui.plugin.v1 protocol. namespace eval ::svw { variable next_id 0 variable handlers variable bindings variable limits variable pending variable granted_capabilities {} variable supported_methods {} variable supported_notifications {} variable supported_values variable surface_clear variable surface_ops variable running 0 array set handlers {} array set bindings {} array set limits {} array set pending {} array set supported_values {} array set surface_clear {} array set surface_ops {} } namespace eval ::svw::layout {} namespace eval ::svw::ui {} namespace eval ::svw::_json { variable text "" variable index 0 variable nodes 0 } proc ::svw::_options {defaults supplied} { if {[llength $supplied] % 2 != 0} { error "option list must contain name/value pairs" } array set options $defaults foreach {name value} $supplied { if {![info exists options($name)]} { error "unknown option: $name" } set options($name) $value } return [array get options] } proc ::svw::quote {value} { set output "\"" foreach char [split $value ""] { scan $char %c code switch -- $char { "\"" { append output "\\\"" } "\\" { append output "\\\\" } "\b" { append output "\\b" } "\f" { append output "\\f" } "\n" { append output "\\n" } "\r" { append output "\\r" } "\t" { append output "\\t" } default { if {$code < 32} { append output [format "\\u%04x" $code] } else { append output $char } } } } append output "\"" return $output } proc ::svw::request {method params {callback ""}} { variable next_id variable limits variable pending incr next_id set frame "{\"jsonrpc\":\"2.0\",\"id\":$next_id,\"method\":[quote $method],\"params\":$params}" if {[info exists limits(max_message_bytes)] && [string bytelength $frame] > $limits(max_message_bytes)} { error "svw request exceeds negotiated max_message_bytes" } if {$callback ne ""} { set pending($next_id) $callback } puts stdout $frame flush stdout return $next_id } proc ::svw::_hello {} { if {![info exists ::env(SVW_PLUGIN_ID)] || ![info exists ::env(SVW_PLUGIN_PROTOCOL)]} { error "svw plugin environment is unavailable" } request session.hello "{\"protocol\":[quote $::env(SVW_PLUGIN_PROTOCOL)],\"plugin\":[quote $::env(SVW_PLUGIN_ID)]}" ::svw::_capture_hello } proc ::svw::_capture_hello {line} { variable limits variable granted_capabilities variable supported_methods variable supported_notifications variable supported_values set document [json_decode $line] foreach name { max_message_bytes max_string_bytes max_text_bytes max_surface_ops max_surfaces max_surface_cells max_surface_update_cells max_surface_dimension max_layout_nodes max_layout_depth max_layout_children max_layout_weight max_layout_gap max_id_bytes max_surface_z max_subscriptions max_messages_per_second max_pending_core_requests max_scope_children_page max_wave_changes_page max_clocked_sample_signals max_clocked_samples_page max_report_lines max_report_bytes handshake_timeout_ms recovery_escape_count recovery_sequence_ms } { if {[catch { _json::typed_value $document [list result limits $name] number } value]} { set value "" } if {$value eq ""} { error "svw hello response omitted limit $name" } set limits($name) $value } set granted_capabilities [_json::string_array $document {result capabilities}] set supported_methods [_json::string_array $document {result methods}] set supported_notifications [_json::string_array $document {result notifications}] foreach category { state_group surface_mode layout_kind draw_op builtin_view input_key input_modifier mouse_kind mouse_button focus_value wave_kind clock_edge } { set supported_values($category) \ [_json::string_array $document [list result values $category]] } } proc ::svw::has_capability {name} { variable granted_capabilities return [expr {[lsearch -exact $granted_capabilities $name] >= 0}] } proc ::svw::limit {name} { variable limits if {![info exists limits($name)]} { error "svw limit is unavailable: $name" } return $limits($name) } proc ::svw::supports_method {name} { variable supported_methods return [expr {[lsearch -exact $supported_methods $name] >= 0}] } proc ::svw::supports_notification {name} { variable supported_notifications return [expr {[lsearch -exact $supported_notifications $name] >= 0}] } proc ::svw::supports_value {category value} { variable supported_values if {![info exists supported_values($category)]} { return false } return [expr {[lsearch -exact $supported_values($category) $value] >= 0}] } proc ::svw::ping {{callback ""}} { request session.ping "{}" $callback } proc ::svw::surface_create {id x y width height args} { variable surface_clear variable surface_ops array set options [_options { -mode overlay -z 0 -opaque false -focusable false -callback {} } $args] set surface_clear($id) false set surface_ops($id) {} request ui.surface.create "{\"surface\":[quote $id],\"mode\":[quote $options(-mode)],\"x\":$x,\"y\":$y,\"width\":$width,\"height\":$height,\"z\":$options(-z),\"opaque\":$options(-opaque),\"focusable\":$options(-focusable)}" $options(-callback) } proc ::svw::surface_clear {id} { variable surface_clear variable surface_ops set surface_clear($id) true set surface_ops($id) {} } proc ::svw::surface_text {id x y text args} { variable limits variable surface_ops array set options [_options {-fg default -bg default} $args] if {[info exists limits(max_text_bytes)] && [string bytelength $text] > $limits(max_text_bytes)} { error "surface text exceeds negotiated max_text_bytes" } lappend surface_ops($id) "{\"op\":\"text\",\"x\":$x,\"y\":$y,\"text\":[quote $text],\"fg\":[quote $options(-fg)],\"bg\":[quote $options(-bg)]}" } proc ::svw::surface_fill {id x y width height args} { variable surface_ops array set options [_options {-fg default -bg default} $args] lappend surface_ops($id) "{\"op\":\"fill\",\"x\":$x,\"y\":$y,\"width\":$width,\"height\":$height,\"fg\":[quote $options(-fg)],\"bg\":[quote $options(-bg)]}" } proc ::svw::surface_commit {id {callback ""}} { variable limits variable surface_clear variable surface_ops set clear false if {[info exists surface_clear($id)]} { set clear $surface_clear($id) } set ops {} if {[info exists surface_ops($id)]} { set ops $surface_ops($id) } if {[info exists limits(max_surface_ops)] && [llength $ops] > $limits(max_surface_ops)} { error "surface update exceeds negotiated max_surface_ops" } set request_id [request ui.surface.update "{\"surface\":[quote $id],\"clear\":$clear,\"ops\":\[[join $ops ,]\]}" $callback] set surface_clear($id) false set surface_ops($id) {} return $request_id } proc ::svw::surface_destroy {id {callback ""}} { variable surface_clear variable surface_ops catch {unset surface_clear($id)} catch {unset surface_ops($id)} request ui.surface.destroy "{\"surface\":[quote $id]}" $callback } proc ::svw::focus {id {callback ""}} { request ui.focus.request "{\"surface\":[quote $id]}" $callback } proc ::svw::focus_release {{callback ""}} { request ui.focus.release "{}" $callback } proc ::svw::layout_replace {root_json {callback ""}} { request ui.layout.replace "{\"root\":$root_json}" $callback } proc ::svw::layout_restore {{callback ""}} { request ui.layout.restore "{}" $callback } proc ::svw::layout::leaf {id view args} { array set options [::svw::_options { -cells -1 -weight 1 -min_width 1 -min_height 1 } $args] set fields [list "\"id\":[::svw::quote $id]" \ "\"view\":[::svw::quote $view]" \ "\"weight\":$options(-weight)" \ "\"min_width\":$options(-min_width)" \ "\"min_height\":$options(-min_height)"] if {$options(-cells) >= 0} { lappend fields "\"cells\":$options(-cells)" } return "{[join $fields ,]}" } proc ::svw::layout::_container {kind id children args} { array set options [::svw::_options { -cells -1 -weight 1 -min_width 1 -min_height 1 -gap 0 } $args] set fields [list "\"id\":[::svw::quote $id]" \ "\"kind\":[::svw::quote $kind]" \ "\"weight\":$options(-weight)" \ "\"min_width\":$options(-min_width)" \ "\"min_height\":$options(-min_height)" \ "\"children\":\[[join $children ,]\]"] if {$kind eq "stack"} { if {$options(-gap) != 0} { error "stack layouts do not accept -gap" } } else { lappend fields "\"gap\":$options(-gap)" } if {$options(-cells) >= 0} { lappend fields "\"cells\":$options(-cells)" } return "{[join $fields ,]}" } proc ::svw::layout::row {id children args} { return [_container row $id $children {*}$args] } proc ::svw::layout::column {id children args} { return [_container column $id $children {*}$args] } proc ::svw::layout::stack {id children args} { return [_container stack $id $children {*}$args] } proc ::svw::ui::label {surface x y text args} { ::svw::surface_text $surface $x $y $text {*}$args } proc ::svw::ui::box {surface x y width height args} { array set options [::svw::_options {-fg palette:7 -bg default} $args] if {$width < 2 || $height < 2} { return } ::svw::surface_text $surface $x $y \ "╭[string repeat ─ [expr {$width - 2}]]╮" \ -fg $options(-fg) -bg $options(-bg) for {set row 1} {$row < $height - 1} {incr row} { ::svw::surface_text $surface $x [expr {$y + $row}] "│" \ -fg $options(-fg) -bg $options(-bg) ::svw::surface_text $surface [expr {$x + $width - 1}] \ [expr {$y + $row}] "│" -fg $options(-fg) -bg $options(-bg) } ::svw::surface_text $surface $x [expr {$y + $height - 1}] \ "╰[string repeat ─ [expr {$width - 2}]]╯" \ -fg $options(-fg) -bg $options(-bg) } proc ::svw::ui::progress {surface x y width fraction args} { array set options [::svw::_options {-fg palette:12 -bg default} $args] set fraction [expr {max(0.0, min(1.0, double($fraction)))}] set filled [expr {int($width * $fraction + 0.5)}] set text "[string repeat █ $filled][string repeat ░ [expr {$width - $filled}]]" ::svw::surface_text $surface $x $y $text \ -fg $options(-fg) -bg $options(-bg) } proc ::svw::ui::list {surface x y values args} { array set options [::svw::_options { -fg palette:7 -bg default -selected -1 -selected_fg palette:12 } $args] set row 0 foreach value $values { set fg [expr {$row == $options(-selected) ? $options(-selected_fg) : $options(-fg)}] ::svw::surface_text $surface $x [expr {$y + $row}] $value \ -fg $fg -bg $options(-bg) incr row } } proc ::svw::ui::button {surface x y label args} { array set options [::svw::_options { -fg palette:12 -bg default -active false } $args] set left {[} set right {]} if {$options(-active)} { set left {>} set right {<} } ::svw::surface_text $surface $x $y "$left $label $right" \ -fg $options(-fg) -bg $options(-bg) } proc ::svw::ui::input {surface x y width value args} { array set options [::svw::_options { -fg palette:7 -bg default -cursor -1 -placeholder "" } $args] set shown $value if {$shown eq ""} { set shown $options(-placeholder) } set shown [string range "$shown[string repeat { } $width]" 0 \ [expr {$width - 1}]] ::svw::surface_text $surface $x $y $shown \ -fg $options(-fg) -bg $options(-bg) if {$options(-cursor) >= 0 && $options(-cursor) < $width} { set character [string index $shown $options(-cursor)] ::svw::surface_text $surface [expr {$x + $options(-cursor)}] $y \ $character -fg $options(-bg) -bg $options(-fg) } } proc ::svw::ui::tabs {surface x y labels selected args} { array set options [::svw::_options { -fg palette:7 -selected_fg palette:12 -bg default } $args] set column $x set index 0 foreach label $labels { set text " $label " set fg [expr {$index == $selected ? $options(-selected_fg) : $options(-fg)}] ::svw::surface_text $surface $column $y $text \ -fg $fg -bg $options(-bg) incr column [string length $text] incr index } } proc ::svw::ui::table {surface x y headers rows args} { array set options [::svw::_options { -fg palette:7 -header_fg palette:12 -bg default -gap 2 } $args] set widths {} set column_count [llength $headers] for {set column 0} {$column < $column_count} {incr column} { set width [string length [lindex $headers $column]] foreach row $rows { set width [expr {max($width, [string length [lindex $row $column]])}] } lappend widths $width } set row_index -1 foreach row [linsert $rows 0 $headers] { set column_x $x for {set column 0} {$column < $column_count} {incr column} { set width [lindex $widths $column] set text [format "%-*s" $width [lindex $row $column]] set fg [expr {$row_index < 0 ? $options(-header_fg) : $options(-fg)}] ::svw::surface_text $surface $column_x \ [expr {$y + $row_index + 1}] $text \ -fg $fg -bg $options(-bg) incr column_x [expr {$width + $options(-gap)}] } incr row_index } } proc ::svw::ui::tree {surface x y nodes args} { array set options [::svw::_options { -fg palette:7 -branch_fg palette:12 -bg default } $args] set row 0 foreach node $nodes { set depth [dict get $node depth] set label [dict get $node label] set count [expr {[dict exists $node count] ? [dict get $node count] : 0}] set expanded [expr {[dict exists $node expanded] && [dict get $node expanded]}] set marker [expr {$count == 0 ? " " : ($expanded ? "▾" : "▸")}] set suffix [expr {$count > 0 ? " ($count)" : ""}] set text "[string repeat { } $depth]$marker $label$suffix" ::svw::surface_text $surface $x [expr {$y + $row}] $text \ -fg $options(-fg) -bg $options(-bg) incr row } } proc ::svw::command {command {callback ""}} { request core.command.invoke "{\"command\":[quote $command]}" $callback } # Transfer control from a plugin row to the exact native signal-panel row. # add is explicit because changing the displayed signal list is a separate # authority decision from selecting a row that is already present. proc ::svw::reveal_signal {signal {add false} {callback ""}} { request core.signal.reveal \ "{\"signal\":[quote $signal],\"add\":$add}" $callback } # Transfer control to the native waveform panel at one native tick when a # plugin record has a time anchor but no meaningful signal anchor. proc ::svw::reveal_time {time {callback ""}} { request core.time.reveal "{\"time\":$time}" $callback } # Atomically reveal the native signal row, move the native waveform cursor, # flash the signal/time intersection, and focus the native wave panel. proc ::svw::reveal_wave {signal time {add false} {callback ""}} { request core.wave.reveal \ "{\"signal\":[quote $signal],\"time\":$time,\"add\":$add}" \ $callback } proc ::svw::report_publish {title lines {callback ""}} { set encoded {} foreach line $lines { lappend encoded [quote $line] } request ui.report.publish \ "{\"title\":[quote $title],\"lines\":\[[join $encoded ,]\]}" \ $callback } proc ::svw::state_get {{callback ""}} { request core.state.get "{}" $callback } proc ::svw::subscribe {events {callback ""}} { set encoded {} foreach event $events { lappend encoded [quote $event] } request event.subscribe "{\"events\":\[[join $encoded ,]\]}" $callback } proc ::svw::unsubscribe {events {callback ""}} { set encoded {} foreach event $events { lappend encoded [quote $event] } request event.unsubscribe "{\"events\":\[[join $encoded ,]\]}" $callback } proc ::svw::wave_value {signal time {callback ""}} { request wave.value.get "{\"signal\":[quote $signal],\"time\":$time}" $callback } proc ::svw::wave_changes {signal start end cursor limit {callback ""}} { request wave.changes.range \ "{\"signal\":[quote $signal],\"start\":$start,\"end\":$end,\"cursor\":[quote $cursor],\"limit\":$limit}" \ $callback } proc ::svw::wave_samples_clocked {clock signals start end edge cursor limit {callback ""}} { set encoded {} foreach signal $signals { lappend encoded [quote $signal] } request wave.samples.clocked \ "{\"clock\":[quote $clock],\"signals\":\[[join $encoded ,]\],\"start\":$start,\"end\":$end,\"edge\":[quote $edge],\"cursor\":[quote $cursor],\"limit\":$limit}" \ $callback } proc ::svw::wave_scope_children {scope offset limit {callback ""}} { request wave.scope.children \ "{\"scope\":[quote $scope],\"offset\":$offset,\"limit\":$limit}" \ $callback } proc ::svw::shutdown {} { variable running set request_id [request session.shutdown "{}"] set running 0 return $request_id } proc ::svw::on {event callback} { variable handlers set handlers($event) $callback } proc ::svw::bind {key callback} { variable bindings set bindings($key) $callback } proc ::svw::unbind {key} { variable bindings catch {unset bindings($key)} } proc ::svw::timer {milliseconds callback} { if {$milliseconds < 0} { error "timer delay must not be negative" } return [after $milliseconds $callback] } proc ::svw::timer_cancel {token} { after cancel $token } proc ::svw::_json::fail {message} { variable index error "$message at character $index" } proc ::svw::_json::skip_space {} { variable text variable index set length [string length $text] while {$index < $length} { set character [string index $text $index] if {$character ne " " && $character ne "\t" && $character ne "\r" && $character ne "\n"} { break } incr index } } proc ::svw::_json::parse_hex4 {} { variable text variable index if {$index + 4 > [string length $text]} { fail "short JSON Unicode escape" } set digits [string range $text $index [expr {$index + 3}]] if {![regexp {^[0-9A-Fa-f]{4}$} $digits]} { fail "invalid JSON Unicode escape" } scan $digits %x value incr index 4 return $value } proc ::svw::_json::codepoint {value} { if {$value <= 0x7f} { set bytes [list $value] } elseif {$value <= 0x7ff} { set bytes [list [expr {0xc0 | ($value >> 6)}] \ [expr {0x80 | ($value & 0x3f)}]] } elseif {$value <= 0xffff} { set bytes [list [expr {0xe0 | ($value >> 12)}] \ [expr {0x80 | (($value >> 6) & 0x3f)}] \ [expr {0x80 | ($value & 0x3f)}]] } else { set bytes [list [expr {0xf0 | ($value >> 18)}] \ [expr {0x80 | (($value >> 12) & 0x3f)}] \ [expr {0x80 | (($value >> 6) & 0x3f)}] \ [expr {0x80 | ($value & 0x3f)}]] } return [encoding convertfrom utf-8 [binary format c* $bytes]] } proc ::svw::_json::parse_string {} { variable text variable index if {[string index $text $index] ne "\""} { fail "expected JSON string" } incr index set output "" set length [string length $text] while {$index < $length} { set character [string index $text $index] incr index if {$character eq "\""} { return $output } scan $character %c code if {$code < 32} { fail "control character in JSON string" } if {$character ne "\\"} { append output $character continue } if {$index >= $length} { fail "short JSON escape" } set escaped [string index $text $index] incr index switch -- $escaped { "\"" { append output "\"" } "\\" { append output "\\" } "/" { append output "/" } "b" { append output "\b" } "f" { append output "\f" } "n" { append output "\n" } "r" { append output "\r" } "t" { append output "\t" } "u" { set value [parse_hex4] if {$value >= 0xd800 && $value <= 0xdbff} { if {$index + 2 > $length || [string range $text $index [expr {$index + 1}]] ne "\\u"} { fail "unpaired high JSON surrogate" } incr index 2 set low [parse_hex4] if {$low < 0xdc00 || $low > 0xdfff} { fail "invalid low JSON surrogate" } set value [expr {0x10000 + (($value - 0xd800) << 10) + ($low - 0xdc00)}] } elseif {$value >= 0xdc00 && $value <= 0xdfff} { fail "unpaired low JSON surrogate" } append output [codepoint $value] } default { fail "unknown JSON escape" } } } fail "unterminated JSON string" } proc ::svw::_json::parse_number {} { variable text variable index set remaining [string range $text $index end] if {![regexp -- {^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?} \ $remaining number]} { fail "invalid JSON number" } incr index [string length $number] return [list number $number] } proc ::svw::_json::parse_array {depth} { variable text variable index incr index skip_space set values {} if {[string index $text $index] eq "]"} { incr index return [list array $values] } while {1} { lappend values [parse_value [expr {$depth + 1}]] skip_space set delimiter [string index $text $index] if {$delimiter eq "]"} { incr index return [list array $values] } if {$delimiter ne ","} { fail "expected comma in JSON array" } incr index } } proc ::svw::_json::parse_object {depth} { variable text variable index incr index skip_space set values {} if {[string index $text $index] eq "\}"} { incr index return [list object $values] } while {1} { skip_space if {[string index $text $index] ne "\""} { fail "expected JSON object key" } set key [parse_string] if {[dict exists $values $key]} { fail "duplicate JSON object key" } skip_space if {[string index $text $index] ne ":"} { fail "expected colon in JSON object" } incr index dict set values $key [parse_value [expr {$depth + 1}]] skip_space set delimiter [string index $text $index] if {$delimiter eq "\}"} { incr index return [list object $values] } if {$delimiter ne ","} { fail "expected comma in JSON object" } incr index } } proc ::svw::_json::parse_value {depth} { variable text variable index variable nodes if {$depth > 64} { fail "JSON nesting limit exceeded" } incr nodes if {$nodes > 100000} { fail "JSON node limit exceeded" } skip_space if {$index >= [string length $text]} { fail "expected JSON value" } set first [string index $text $index] switch -- $first { \{ { return [parse_object $depth] } \[ { return [parse_array $depth] } "\"" { return [list string [parse_string]] } "t" { if {[string range $text $index [expr {$index + 3}]] ne "true"} { fail "invalid JSON value" } incr index 4 return [list bool 1] } "f" { if {[string range $text $index [expr {$index + 4}]] ne "false"} { fail "invalid JSON value" } incr index 5 return [list bool 0] } "n" { if {[string range $text $index [expr {$index + 3}]] ne "null"} { fail "invalid JSON value" } incr index 4 return [list null {}] } default { return [parse_number] } } } proc ::svw::json_decode {document} { variable limits set maximum 1048576 if {[info exists limits(max_message_bytes)]} { set maximum $limits(max_message_bytes) } if {[string bytelength $document] > $maximum} { error "JSON document exceeds negotiated max_message_bytes" } set ::svw::_json::text $document set ::svw::_json::index 0 set ::svw::_json::nodes 0 set value [_json::parse_value 0] _json::skip_space if {$::svw::_json::index != [string length $::svw::_json::text]} { _json::fail "trailing JSON content" } return $value } proc ::svw::json_type {value} { if {[llength $value] != 2 || [lsearch -exact {null bool number string array object} [lindex $value 0]] < 0} { error "invalid decoded JSON value" } return [lindex $value 0] } proc ::svw::json_payload {value} { json_type $value return [lindex $value 1] } proc ::svw::_json::try_get {document path output_name} { upvar 1 $output_name output set current $document foreach component $path { set type [::svw::json_type $current] set payload [::svw::json_payload $current] if {$type eq "object"} { if {![dict exists $payload $component]} { return 0 } set current [dict get $payload $component] } elseif {$type eq "array" && [string is integer -strict $component] && $component >= 0 && $component < [llength $payload]} { set current [lindex $payload $component] } else { return 0 } } set output $current return 1 } proc ::svw::json_get {document path} { if {![_json::try_get $document $path value]} { error "JSON path is unavailable: [join $path .]" } return $value } proc ::svw::_json::typed_value {document path expected} { set value [::svw::json_get $document $path] set actual [::svw::json_type $value] if {$actual ne $expected} { error "JSON path [join $path .] has type $actual, expected $expected" } return [::svw::json_payload $value] } proc ::svw::_json::string_array {document path} { set values [typed_value $document $path array] set output {} foreach value $values { if {[::svw::json_type $value] ne "string"} { error "JSON path [join $path .] is not a string array" } lappend output [::svw::json_payload $value] } return $output } proc ::svw::string_field {line path} { if {[catch {_json::typed_value [json_decode $line] $path string} value]} { return "" } return $value } proc ::svw::number_field {line path} { if {[catch {_json::typed_value [json_decode $line] $path number} value]} { return "" } return $value } proc ::svw::bool_field {line path} { if {[catch {_json::typed_value [json_decode $line] $path bool} value]} { return "" } return $value } proc ::svw::string_array_field {line path} { if {[catch {_json::string_array [json_decode $line] $path} value]} { return {} } return $value } proc ::svw::_readable {} { variable bindings variable handlers variable pending variable running if {[gets stdin line] < 0} { if {[eof stdin]} { set running 0 } return } if {[catch {json_decode $line} document]} { puts stderr "svw plugin received invalid JSON: $document" set running 0 return } set method "" catch {set method [_json::typed_value $document {method} string]} if {$method eq ""} { set id "" if {[_json::try_get $document {id} id_value] && [json_type $id_value] in {number string}} { set id [json_payload $id_value] } if {[_json::try_get $document {error} error_value]} { set message "unknown protocol error" catch { set message [_json::typed_value $document {error message} string] } puts stderr "svw plugin request failed: $message" } if {$id ne "" && [info exists pending($id)]} { set callback $pending($id) unset pending($id) uplevel #0 [list {*}$callback $line] } return } set event [dict create type $method] if {$method eq "ui.resize"} { dict set event width [_json::typed_value $document {params width} number] dict set event height [_json::typed_value $document {params height} number] } elseif {$method eq "ui.layout"} { dict set event revision [_json::typed_value $document {params revision} number] dict set event generation [_json::typed_value $document {params generation} number] dict set event placed [_json::typed_value $document {params placed} bool] set node "" catch {set node [_json::typed_value $document {params node} string]} dict set event node $node dict set event surface [_json::typed_value $document {params surface} string] dict set event x [_json::typed_value $document {params x} number] dict set event y [_json::typed_value $document {params y} number] dict set event width [_json::typed_value $document {params width} number] dict set event height [_json::typed_value $document {params height} number] } elseif {$method eq "ui.input"} { dict set event surface [_json::typed_value $document {params surface} string] dict set event key [_json::typed_value $document {params key} string] dict set event modifiers [_json::string_array $document {params modifiers}] foreach field {character mouse_kind button} { set value "" catch {set value [_json::typed_value $document [list params $field] string]} dict set event $field $value } foreach field {x y} { set value "" catch {set value [_json::typed_value $document [list params $field] number]} dict set event $field $value } } elseif {$method eq "ui.focus.changed"} { dict set event focused [_json::typed_value $document {params focused} bool] set surface "" catch {set surface [_json::typed_value $document {params surface} string]} dict set event surface $surface } elseif {$method eq "state.changed"} { dict set event revision [_json::typed_value $document {params revision} number] dict set event changed [_json::string_array $document {params changed}] foreach field {file focus format selected_signal timescale} { dict set event $field \ [_json::typed_value $document [list params state $field] string] } foreach field {cursor minimum_time maximum_time view_start view_end wave_revision wave_progress} { dict set event $field \ [_json::typed_value $document [list params state $field] number] } dict set event wave_loading \ [_json::typed_value $document {params state wave_loading} bool] } if {$method eq "ui.input"} { set binding [dict get $event key] if {$binding eq "char" && [dict get $event character] ne ""} { set binding "char:[dict get $event character]" } if {[info exists bindings($binding)]} { uplevel #0 [list {*}$bindings($binding) $event] } } if {[info exists handlers($method)]} { uplevel #0 [list {*}$handlers($method) $event] } } proc ::svw::run {} { variable running fconfigure stdin -blocking 0 -buffering line -translation lf -encoding utf-8 fconfigure stdout -buffering line -translation lf -encoding utf-8 _hello set running 1 fileevent stdin readable ::svw::_readable vwait ::svw::running }