4runr-os-mk3 0.1.0 → 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/src/ui/mod.rs ADDED
@@ -0,0 +1,4 @@
1
+ pub mod layout;
2
+ pub mod safe_viewport;
3
+ pub mod boot;
4
+
@@ -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
+