4runr-os-mk3 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1105 -0
- package/Cargo.toml +16 -0
- package/install.js +37 -11
- package/package.json +4 -1
- package/src/app/render_scheduler.rs +103 -0
- package/src/app.rs +435 -0
- package/src/io/mod.rs +66 -0
- package/src/io/protocol.rs +15 -0
- package/src/io/stdio.rs +32 -0
- package/src/io/ws.rs +32 -0
- package/src/main.rs +119 -0
- package/src/ui/boot.rs +150 -0
- package/src/ui/layout.rs +705 -0
- package/src/ui/mod.rs +4 -0
- package/src/ui/safe_viewport.rs +235 -0
package/src/ui/mod.rs
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
use ratatui::prelude::*;
|
|
2
|
+
use std::env;
|
|
3
|
+
|
|
4
|
+
/// Render mode: Local (full borders) vs Browser (no outer borders)
|
|
5
|
+
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
6
|
+
#[allow(dead_code)]
|
|
7
|
+
pub enum RenderMode {
|
|
8
|
+
Local, // Full borders, rich UI
|
|
9
|
+
Browser, // No outer borders, larger margins
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
impl RenderMode {
|
|
13
|
+
pub fn detect() -> Self {
|
|
14
|
+
// Allow explicit override
|
|
15
|
+
if let Ok(mode) = env::var("TUI_RENDER_MODE") {
|
|
16
|
+
match mode.to_lowercase().as_str() {
|
|
17
|
+
"local" => return RenderMode::Local,
|
|
18
|
+
"browser" | "web" => return RenderMode::Browser,
|
|
19
|
+
_ => {}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Auto-detect
|
|
24
|
+
if is_cloudshell_mode() {
|
|
25
|
+
RenderMode::Browser
|
|
26
|
+
} else {
|
|
27
|
+
RenderMode::Local
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Safe viewport configuration from environment variables
|
|
33
|
+
#[derive(Debug, Clone)]
|
|
34
|
+
pub struct SafeViewportConfig {
|
|
35
|
+
pub margin_x: u16,
|
|
36
|
+
pub margin_y: u16,
|
|
37
|
+
pub guard_right: u16,
|
|
38
|
+
pub guard_bottom: u16,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
impl Default for SafeViewportConfig {
|
|
42
|
+
fn default() -> Self {
|
|
43
|
+
let render_mode = RenderMode::detect();
|
|
44
|
+
|
|
45
|
+
match render_mode {
|
|
46
|
+
RenderMode::Browser => Self {
|
|
47
|
+
margin_x: env::var("TUI_MARGIN_X")
|
|
48
|
+
.ok()
|
|
49
|
+
.and_then(|v| v.parse().ok())
|
|
50
|
+
.unwrap_or(2),
|
|
51
|
+
margin_y: env::var("TUI_MARGIN_Y")
|
|
52
|
+
.ok()
|
|
53
|
+
.and_then(|v| v.parse().ok())
|
|
54
|
+
.unwrap_or(1),
|
|
55
|
+
guard_right: env::var("TUI_GUARD_RIGHT")
|
|
56
|
+
.ok()
|
|
57
|
+
.and_then(|v| v.parse().ok())
|
|
58
|
+
.unwrap_or(6), // Was 4, increase to 6
|
|
59
|
+
guard_bottom: env::var("TUI_GUARD_BOTTOM")
|
|
60
|
+
.ok()
|
|
61
|
+
.and_then(|v| v.parse().ok())
|
|
62
|
+
.unwrap_or(3), // Was 2, increase to 3
|
|
63
|
+
},
|
|
64
|
+
RenderMode::Local => Self {
|
|
65
|
+
margin_x: env::var("TUI_MARGIN_X")
|
|
66
|
+
.ok()
|
|
67
|
+
.and_then(|v| v.parse().ok())
|
|
68
|
+
.unwrap_or(0), // Was 1, no margin needed locally
|
|
69
|
+
margin_y: env::var("TUI_MARGIN_Y")
|
|
70
|
+
.ok()
|
|
71
|
+
.and_then(|v| v.parse().ok())
|
|
72
|
+
.unwrap_or(0),
|
|
73
|
+
guard_right: env::var("TUI_GUARD_RIGHT")
|
|
74
|
+
.ok()
|
|
75
|
+
.and_then(|v| v.parse().ok())
|
|
76
|
+
.unwrap_or(1), // Minimal guard
|
|
77
|
+
guard_bottom: env::var("TUI_GUARD_BOTTOM")
|
|
78
|
+
.ok()
|
|
79
|
+
.and_then(|v| v.parse().ok())
|
|
80
|
+
.unwrap_or(1),
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Detect if running in CloudShell/browser terminal
|
|
87
|
+
pub fn is_cloudshell_mode() -> bool {
|
|
88
|
+
// AWS CloudShell
|
|
89
|
+
env::var("AWS_EXECUTION_ENV").is_ok()
|
|
90
|
+
|| env::var("CLOUDSHELL").is_ok()
|
|
91
|
+
// Google Cloud Shell
|
|
92
|
+
|| env::var("CLOUD_SHELL").is_ok()
|
|
93
|
+
|| env::var("DEVSHELL_PROJECT_ID").is_ok()
|
|
94
|
+
// Azure Cloud Shell
|
|
95
|
+
|| env::var("ACC_CLOUD").is_ok()
|
|
96
|
+
// Generic web terminal indicators
|
|
97
|
+
|| env::var("TUI_WEB").map(|v| v == "1").unwrap_or(false)
|
|
98
|
+
|| env::var("TERM_PROGRAM").map(|v| v.contains("xterm.js")).unwrap_or(false)
|
|
99
|
+
// SSH through web console (likely browser-based)
|
|
100
|
+
|| (env::var("SSH_CONNECTION").is_ok() && env::var("DISPLAY").is_err())
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Safe viewport calculation result
|
|
104
|
+
#[derive(Debug, Clone)]
|
|
105
|
+
#[allow(dead_code)]
|
|
106
|
+
pub struct SafeViewport {
|
|
107
|
+
pub canvas_cols: u16,
|
|
108
|
+
pub canvas_rows: u16,
|
|
109
|
+
pub safe_cols: u16,
|
|
110
|
+
pub safe_rows: u16,
|
|
111
|
+
pub safe_rect: Rect,
|
|
112
|
+
pub config: SafeViewportConfig,
|
|
113
|
+
pub render_mode: RenderMode,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
impl SafeViewport {
|
|
117
|
+
/// Calculate safe viewport from terminal size
|
|
118
|
+
pub fn new(full_area: Rect) -> Self {
|
|
119
|
+
let render_mode = RenderMode::detect();
|
|
120
|
+
let config = SafeViewportConfig::default();
|
|
121
|
+
|
|
122
|
+
let canvas_cols = full_area.width;
|
|
123
|
+
let canvas_rows = full_area.height;
|
|
124
|
+
|
|
125
|
+
// Calculate safe area with margins and guards
|
|
126
|
+
// Hard rule: never draw on last column/row
|
|
127
|
+
let left_margin = config.margin_x;
|
|
128
|
+
let right_margin = config.margin_x + config.guard_right;
|
|
129
|
+
let top_margin = config.margin_y;
|
|
130
|
+
let bottom_margin = config.margin_y + config.guard_bottom;
|
|
131
|
+
|
|
132
|
+
let safe_x = full_area.x + left_margin;
|
|
133
|
+
let safe_y = full_area.y + top_margin;
|
|
134
|
+
let safe_w = full_area.width
|
|
135
|
+
.saturating_sub(left_margin + right_margin)
|
|
136
|
+
.max(1);
|
|
137
|
+
let safe_h = full_area.height
|
|
138
|
+
.saturating_sub(top_margin + bottom_margin)
|
|
139
|
+
.max(1);
|
|
140
|
+
|
|
141
|
+
let safe_rect = Rect {
|
|
142
|
+
x: safe_x,
|
|
143
|
+
y: safe_y,
|
|
144
|
+
width: safe_w,
|
|
145
|
+
height: safe_h,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
Self {
|
|
149
|
+
canvas_cols,
|
|
150
|
+
canvas_rows,
|
|
151
|
+
safe_cols: safe_w,
|
|
152
|
+
safe_rows: safe_h,
|
|
153
|
+
safe_rect,
|
|
154
|
+
config,
|
|
155
|
+
render_mode,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// Check if terminal is too small for minimum requirements
|
|
160
|
+
pub fn is_too_small(&self, required_cols: u16, required_rows: u16) -> bool {
|
|
161
|
+
self.safe_cols < required_cols || self.safe_rows < required_rows
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Validate a rectangle before rendering
|
|
165
|
+
#[allow(dead_code)]
|
|
166
|
+
pub fn validate_rect(&self, rect: Rect, min_w: u16, min_h: u16, name: &str) -> RectValidation {
|
|
167
|
+
// Check minimum size
|
|
168
|
+
if rect.width < min_w || rect.height < min_h {
|
|
169
|
+
return RectValidation::Invalid {
|
|
170
|
+
reason: format!(
|
|
171
|
+
"{}: size {}x{} below minimum {}x{}",
|
|
172
|
+
name, rect.width, rect.height, min_w, min_h
|
|
173
|
+
),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check bounds: rect must fit within safe_rect (using ABSOLUTE coordinates)
|
|
178
|
+
let max_x = rect.x + rect.width;
|
|
179
|
+
let max_y = rect.y + rect.height;
|
|
180
|
+
let safe_max_x = self.safe_rect.x + self.safe_rect.width; // FIXED: use absolute bound
|
|
181
|
+
let safe_max_y = self.safe_rect.y + self.safe_rect.height; // FIXED: use absolute bound
|
|
182
|
+
|
|
183
|
+
if max_x > safe_max_x {
|
|
184
|
+
return RectValidation::Invalid {
|
|
185
|
+
reason: format!(
|
|
186
|
+
"{}: right edge {} exceeds safe bound {}",
|
|
187
|
+
name, max_x, safe_max_x
|
|
188
|
+
),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if max_y > safe_max_y {
|
|
193
|
+
return RectValidation::Invalid {
|
|
194
|
+
reason: format!(
|
|
195
|
+
"{}: bottom edge {} exceeds safe bound {}",
|
|
196
|
+
name, max_y, safe_max_y
|
|
197
|
+
),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Clamp to safe bounds
|
|
202
|
+
let clamped = rect.intersection(self.safe_rect);
|
|
203
|
+
|
|
204
|
+
if clamped.width == 0 || clamped.height == 0 {
|
|
205
|
+
return RectValidation::Invalid {
|
|
206
|
+
reason: format!("{}: clamped rect has zero size", name),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
RectValidation::Valid(clamped)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
#[derive(Debug, Clone)]
|
|
215
|
+
#[allow(dead_code)]
|
|
216
|
+
pub enum RectValidation {
|
|
217
|
+
Valid(Rect),
|
|
218
|
+
Invalid { reason: String },
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
impl RectValidation {
|
|
222
|
+
#[allow(dead_code)]
|
|
223
|
+
pub fn is_valid(&self) -> bool {
|
|
224
|
+
matches!(self, RectValidation::Valid(_))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#[allow(dead_code)]
|
|
228
|
+
pub fn unwrap_or_default(self) -> Rect {
|
|
229
|
+
match self {
|
|
230
|
+
RectValidation::Valid(rect) => rect,
|
|
231
|
+
RectValidation::Invalid { .. } => Rect::default(),
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|