docs: explain shell activity and platform support boundaries
This commit is contained in:
@@ -18,15 +18,22 @@ const MAX_RECORDS: usize = 80;
|
||||
#[derive(Clone, Debug)]
|
||||
/// 一次 HTTP 请求/响应的有界观察记录,供 Network 工具查看。
|
||||
pub struct Exchange {
|
||||
// 由 begin 分配的日志身份,HTTP worker 后续只携带此 ID 更新,不持有队列元素引用。
|
||||
pub id: u64,
|
||||
// 请求方法和来源标签由本地 API 观察器传入;origin 只供显示,不授予执行权限。
|
||||
pub method: String,
|
||||
// 请求 URL 的受限预览,不在此解析路由或读取所指文件。
|
||||
pub path: String,
|
||||
pub origin: String,
|
||||
// 开始时刻的 UTC 时分秒文本,不含日期;耗时另用 started 单调时钟计算。
|
||||
pub timestamp: String,
|
||||
// 初始为空,成功读取请求体后 body 覆盖预览;不是用于执行脚本的原始正文存储。
|
||||
pub request: String,
|
||||
// finish 记录已构造响应的正文预览,即使后续 socket 发送失败也可能有值。
|
||||
pub response: String,
|
||||
// None 表示尚未记录响应;完成后保存 HTTP 状态码,不据此判断业务调用是否成功。
|
||||
pub status: Option<u16>,
|
||||
// 初始为零,finish 时从 begin 起累计毫秒;不在界面每次绘制时持续刷新。
|
||||
pub elapsed_ms: u128,
|
||||
pub request_headers: String,
|
||||
pub response_headers: String,
|
||||
@@ -37,12 +44,15 @@ pub struct Exchange {
|
||||
#[derive(Debug, Default)]
|
||||
/// 按容量维护近期 HTTP 活动与请求身份,防止无限日志积累。
|
||||
pub struct ActivityLog {
|
||||
// 开始顺序队列,Network/Console 工具通过共享锁读取或复制;完成顺序不会重排记录。
|
||||
pub records: VecDeque<Exchange>,
|
||||
next_id: u64,
|
||||
}
|
||||
impl ActivityLog {
|
||||
/// 为请求分配活动 ID 并登记其方法、来源和受限头部。
|
||||
// runtime/api 的 ObservedRequest 构造时调用;先写日志再由调用方通知界面,本模块不主动唤醒窗口。
|
||||
pub fn begin(&mut self, method: String, path: String, origin: String, headers: String) -> u64 {
|
||||
// ID 在当前日志实例内递增,不从队列长度推导;淘汰旧项不会重置计数。
|
||||
self.next_id += 1;
|
||||
let id = self.next_id;
|
||||
// 显示时间取 Unix 秒数的当日余数;系统时间早于纪元时回退到零。这里只显示时间,不保存日期。
|
||||
@@ -51,6 +61,7 @@ impl ActivityLog {
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
% 86400;
|
||||
// 登记开始状态,正文和响应留空,started 与墙上时钟分别服务耗时和时间显示。
|
||||
self.records.push_back(Exchange {
|
||||
id,
|
||||
method,
|
||||
@@ -78,13 +89,16 @@ impl ActivityLog {
|
||||
}
|
||||
/// 更新请求正文预览,内容长度仍由活动记录策略限制。
|
||||
// 线性查找在最多 80 项范围内完成;本方法覆盖之前的请求预览,不拼接分块正文。
|
||||
// ObservedRequest::record_body 在读取请求体后调用,后续 submit 仍使用原正文,日志只保存截断副本。
|
||||
pub fn body(&mut self, id: u64, body: &str) {
|
||||
// 按 ID 更新仍在队列内的记录;请求被容量淘汰时不重建,以免旧请求挤走较新的观察记录。
|
||||
if let Some(record) = self.records.iter_mut().find(|r| r.id == id) {
|
||||
record.request = preview(body);
|
||||
}
|
||||
}
|
||||
/// 为指定请求登记响应状态、正文/头部预览与单调时钟耗时;记录已淘汰时忽略。
|
||||
// 仅更新仍保留的请求,响应正文和头部都重新截断;耗时从 begin 起计算。
|
||||
// ObservedRequest::respond 在真实响应发送前调用,状态和耗时表示已构造响应,不证明客户端接收成功。
|
||||
pub fn finish(&mut self, id: u64, status: u16, body: &str, headers: String) {
|
||||
if let Some(record) = self.records.iter_mut().find(|r| r.id == id) {
|
||||
record.status = Some(status);
|
||||
@@ -96,12 +110,15 @@ impl ActivityLog {
|
||||
}
|
||||
|
||||
/// 截取可显示的受限文本预览,避免日志无限保留完整大正文。
|
||||
// begin/body/finish 统一使用 UTF-8 字节预算;输入未超限时仍返回独立 String,不借用请求缓冲。
|
||||
pub fn preview(value: &str) -> String {
|
||||
// 恰好等于上限也保留原文,不追加截断标记;长度判断按字节而不是字符数量。
|
||||
if value.len() <= BODY_PREVIEW {
|
||||
return value.to_owned();
|
||||
}
|
||||
// UTF-8 多字节字符不能从中间切开;从字节上限回退到合法边界,确保输出始终是有效字符串。
|
||||
let mut end = BODY_PREVIEW;
|
||||
// 最多回退到当前 UTF-8 字符起点,只保证字符边界,不承诺完整字素簇边界。
|
||||
while !value.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
use anyhow::{Result, bail};
|
||||
#[cfg(target_os = "macos")]
|
||||
/// 构造固定 UTF-8 环境的系统命令;本函数不启动进程,退出状态由 read/write 检查。
|
||||
// 仅 macOS 的 read/write 使用,构造独立 Command 并覆盖编码环境,不修改宿主进程全局环境变量。
|
||||
fn command(program: &str) -> std::process::Command {
|
||||
let mut cmd = std::process::Command::new(program);
|
||||
// 从 Finder 等无 LANG 的环境启动时也显式指定 UTF-8,防止系统工具按其他编码转换中文。
|
||||
@@ -14,6 +15,7 @@ fn command(program: &str) -> std::process::Command {
|
||||
cmd
|
||||
}
|
||||
/// 取得系统剪贴板的 UTF-8 文本,失败作为可显示错误返回。
|
||||
// shell/app 处理开发工具粘贴快捷键时调用;失败由工具 input_error 展示,不用空字符串冒充成功。
|
||||
pub fn read() -> Result<String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -22,18 +24,22 @@ pub fn read() -> Result<String> {
|
||||
.arg("-Prefer")
|
||||
.arg("txt")
|
||||
.output()?;
|
||||
// 进程正常退出但状态非成功仍为错误,不能把可能残缺的 stdout 当作源代码粘贴。
|
||||
if !output.status.success() {
|
||||
bail!("system clipboard read failed");
|
||||
}
|
||||
// Never silently replace invalid bytes in the user's source.
|
||||
// 严格验证输出 UTF-8,非法字节报错而不是替换为 U+FFFD 后悄悄改变用户文本。
|
||||
Ok(String::from_utf8(output.stdout)?)
|
||||
}
|
||||
// 平台分支在编译时选择,非 macOS 不尝试猜测其他系统剪贴板命令。
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
bail!("system clipboard adapter is not available on this platform")
|
||||
}
|
||||
}
|
||||
/// 将提供的文本交给系统剪贴板命令,不执行文本内容。
|
||||
// shell/app 的复制与剪切路径调用;只有成功返回后剪切才会删除编辑器中的选中文本。
|
||||
pub fn write(value: &str) -> Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -42,13 +48,16 @@ pub fn write(value: &str) -> Result<()> {
|
||||
let mut child = command("/usr/bin/pbcopy")
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.spawn()?;
|
||||
// 取走子进程管道句柄,后面可显式 drop 发送 EOF,不能让 pbcopy 一直等待更多输入。
|
||||
let mut stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("clipboard stdin is unavailable"))?;
|
||||
// 按 UTF-8 原字节写完整内容;写管道失败立即传播,此函数该错误路径没有另外执行 wait。
|
||||
stdin.write_all(value.as_bytes())?;
|
||||
// 写完后先关闭管道发送 EOF,让 pbcopy 知道输入结束;之后等待退出状态,不能仅以写管道成功作为完成。
|
||||
drop(stdin);
|
||||
// 等待真实系统工具退出并检查状态;写入成功和剪贴板操作完成是两个独立检查。
|
||||
if !child.wait()?.success() {
|
||||
bail!("system clipboard write failed");
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
//! 原生宿主模块入口。窗口、管理控件、系统适配和开发工具状态与用户业务界面分开维护。
|
||||
//!
|
||||
// 目录确定模块归属;main 中的 crate 根别名保持内部调用路径稳定。
|
||||
// app 在原生事件循环中协调这些状态,并通过共享数据和输入协议与 runtime 衔接;本文件只声明模块,不启动线程或窗口。
|
||||
/// 保存运行活动、请求和诊断记录,供宿主开发工具展示。
|
||||
pub(crate) mod activity;
|
||||
/// 实现 winit 应用生命周期、窗口协调、输入路由和 softbuffer 像素呈现。
|
||||
@@ -14,7 +15,7 @@ pub(crate) mod developer_tools;
|
||||
pub(crate) mod native_chrome;
|
||||
/// 维护页面/工具入口列表、折叠宽度和命中目标;可用页面宽度需扣除其占用。
|
||||
pub(crate) mod sidebar;
|
||||
/// 从源码与诊断生成可浏览的结构信息,供 Sources 工具定位对象和文本。
|
||||
/// 定义云文件 provider 和 Sources 工作区状态,供工具列出文件并只读打开内容。
|
||||
pub(crate) mod sources;
|
||||
/// 提供宿主管理界面专用的主题、控件和交互状态,和 Endor 业务界面共享底层绘图能力。
|
||||
pub(crate) mod system_ui;
|
||||
|
||||
@@ -12,6 +12,7 @@ use winit::{
|
||||
/// The custom draggable row does not receive AppKit's titlebar double-click
|
||||
/// action automatically. Use the native click count and native window actions.
|
||||
/// 读取系统偏好的标题栏双击行为,委托原生窗口执行相应动作。
|
||||
// shell/app 在自绘标题控制区左键按下时调用;true 表示本次双击已处理,调用方不再开始普通窗口拖动。
|
||||
pub fn titlebar_double_click(window: &Window) -> bool {
|
||||
// AppKit 必须在主线程调用;不在主线程、无有效句柄或句柄非 AppKit 时返回未处理。
|
||||
let Some(main_thread) = MainThreadMarker::new() else {
|
||||
@@ -26,6 +27,7 @@ pub fn titlebar_double_click(window: &Window) -> bool {
|
||||
// SAFETY: called on the event-loop thread during a left-button press, with
|
||||
// a live winit-owned NSView. No native object outlives the Window borrow.
|
||||
unsafe {
|
||||
// 从当前 AppKit 事件读取原生 clickCount;无事件就不猜测双击,也不执行窗口动作。
|
||||
let Some(event) = NSApplication::sharedApplication(main_thread).currentEvent() else {
|
||||
return false;
|
||||
};
|
||||
@@ -33,6 +35,7 @@ pub fn titlebar_double_click(window: &Window) -> bool {
|
||||
if event.clickCount() != 2 {
|
||||
return false;
|
||||
}
|
||||
// 借用 winit 提供的活动 NSView,再取得所属 NSWindow;不创建窗口或把原生指针保存到外部状态。
|
||||
let view = &*handle.ns_view.as_ptr().cast::<NSView>();
|
||||
let Some(native) = view.window() else {
|
||||
return false;
|
||||
@@ -41,6 +44,7 @@ pub fn titlebar_double_click(window: &Window) -> bool {
|
||||
if native.styleMask().contains(NSWindowStyleMask::FullScreen) {
|
||||
return true;
|
||||
}
|
||||
// 读取系统标题栏双击设置,字符串转成 Rust 自有值后按约定选原生动作。
|
||||
let action = NSUserDefaults::standardUserDefaults()
|
||||
.stringForKey(&NSString::from_str("AppleActionOnDoubleClick"))
|
||||
.map(|value| value.to_string());
|
||||
@@ -58,7 +62,9 @@ pub fn titlebar_double_click(window: &Window) -> bool {
|
||||
/// Called only on winit's main thread, after native creation and before drawing.
|
||||
/// AppKit can restore its default button positions when laying out the title bar.
|
||||
/// 调整真实 macOS 窗口三键的位置,与全尺寸内容和侧栏控制区对齐。
|
||||
// shell/app 创建窗口后和绘制前调用;主线程调用由外层保证,此函数本身没有创建 MainThreadMarker 检查。
|
||||
pub fn align_controls(window: &Window) {
|
||||
// 取得当前 Window 的句柄,失败或非 AppKit 平台表示无法调整,返回不改变现有原生布局。
|
||||
let Ok(handle) = window.window_handle() else {
|
||||
return;
|
||||
};
|
||||
@@ -76,6 +82,7 @@ pub fn align_controls(window: &Window) {
|
||||
if native.styleMask().contains(NSWindowStyleMask::FullScreen) {
|
||||
return;
|
||||
}
|
||||
// 读取内容视图边界,用与侧栏相同的 HEADER 高度确定标题行中心。
|
||||
let bounds = view.bounds();
|
||||
let center = f64::from(crate::sidebar::HEADER) / 2.0;
|
||||
// AppKit 视图可能以左下或左上为原点;先在内容视图内算标题行中心,再转到按钮父视图。
|
||||
@@ -84,19 +91,23 @@ pub fn align_controls(window: &Window) {
|
||||
} else {
|
||||
bounds.origin.y + bounds.size.height - center
|
||||
};
|
||||
// 只处理关闭、最小化和缩放三个真实系统按钮,不创建伪按钮代替原生交互。
|
||||
for kind in [
|
||||
NSWindowButton::NSWindowCloseButton,
|
||||
NSWindowButton::NSWindowMiniaturizeButton,
|
||||
NSWindowButton::NSWindowZoomButton,
|
||||
] {
|
||||
// 系统样式可能不提供某个按钮,缺失时跳过该项,其他按钮仍继续对齐。
|
||||
let Some(button) = native.standardWindowButton(kind) else {
|
||||
continue;
|
||||
};
|
||||
// 需要按钮父视图做坐标转换;没有父视图时不能直接写内容视图坐标。
|
||||
let Some(parent) = button.superview() else {
|
||||
continue;
|
||||
};
|
||||
// 按钮可能位于不同 AppKit 容器,必须使用 convertPoint 而不能直接赋内容视图坐标。
|
||||
let target = parent.convertPoint_fromView(NSPoint::new(0.0, content_y), Some(view));
|
||||
// 保留当前大小与横坐标,仅把目标中心减半高换算成按钮纵向原点。
|
||||
let frame = button.frame();
|
||||
let y = target.y - frame.size.height / 2.0;
|
||||
// Preserve native button size, horizontal spacing and behavior.
|
||||
|
||||
@@ -16,7 +16,9 @@ pub struct SourceFile {
|
||||
#[derive(Clone, Debug)]
|
||||
/// 已经取得的源文件内容与标识,不代表它已经被执行。
|
||||
pub struct OpenedSource {
|
||||
// provider 返回的展示路径,Sources 面板据此显示当前打开文件标题,不把它当作执行入口。
|
||||
pub path: String,
|
||||
// 已读取的文本副本,仅绘制源码行;不覆盖当前已发布文档的 source 或触发热重载。
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
@@ -33,12 +35,16 @@ pub trait CloudSourceProvider: std::fmt::Debug + Send {
|
||||
pub struct SourceWorkspace {
|
||||
// None 表示未接入外部文件服务;默认工作区不提供虚构文件或隐式认证。
|
||||
pub provider: Option<Box<dyn CloudSourceProvider>>,
|
||||
// 最近一次 list_files 成功返回的完整列表,刷新失败仍保留供界面查看。
|
||||
pub files: Vec<SourceFile>,
|
||||
// 最近成功打开的内容;选择另一个文件失败时保持旧内容,不显示半读取结果。
|
||||
pub opened: Option<OpenedSource>,
|
||||
// 最近调用错误的可显示文本,成功 refresh/open 会清空;默认未连接时初始为空。
|
||||
pub error: String,
|
||||
}
|
||||
impl SourceWorkspace {
|
||||
/// 更新云文件列表并保存结果或错误,供 Sources 页面显示。
|
||||
// developer_tools 处理 Action::CloudRefresh 时调用,build 仅根据保存状态绘制,不在绘制中请求 provider。
|
||||
pub fn refresh(&mut self) {
|
||||
// 未连接时明确写错误并返回;已有文件列表/打开内容不会在失败路径中被清除。
|
||||
let Some(provider) = &mut self.provider else {
|
||||
@@ -47,6 +53,7 @@ impl SourceWorkspace {
|
||||
};
|
||||
// 调用是同步的,调用方负责安排执行位置;成功才整体替换列表,失败保留旧列表供查看。
|
||||
match provider.list_files() {
|
||||
// 只替换列表并清错,不自动重读或关闭已经打开的文件。
|
||||
Ok(files) => {
|
||||
self.files = files;
|
||||
self.error.clear();
|
||||
@@ -56,12 +63,14 @@ impl SourceWorkspace {
|
||||
}
|
||||
/// 更新选中文件的内容与打开状态,失败保持可读错误信息。
|
||||
// 只读取内容而不执行代码;未连接时直接返回,不像 refresh 那样写入新的错误。
|
||||
// developer_tools 处理 Action::CloudOpen(id) 时调用,id 来自 SourceFile,不用显示路径代替 provider 身份。
|
||||
pub fn open(&mut self, id: &str) {
|
||||
let Some(provider) = &mut self.provider else {
|
||||
return;
|
||||
};
|
||||
// 成功替换 opened 并清错;失败只更新 error,用户仍可查看之前成功打开的内容。
|
||||
match provider.open_file(id) {
|
||||
// provider 返回完整内容后一次替换 opened,查看动作不会执行其中的 Endor 程序。
|
||||
Ok(source) => {
|
||||
self.opened = Some(source);
|
||||
self.error.clear();
|
||||
|
||||
@@ -6,12 +6,14 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
/// 托管页面注册表的不可变投影,原生工具只使用稳定 ID 与显示元数据。
|
||||
// ui_bridge::project_tabs 从当前窗口页面注册表生成;sidebar 画标签,scene 用 developer_tool 标记选择宿主管理页。
|
||||
pub struct Tab {
|
||||
// 会话内稳定 ID,供原生侧发起切页/关闭操作;不能直接当作 VM 对象地址。
|
||||
pub id: u64,
|
||||
// 本次发布的标题文本副本,原生界面显示时无需读取 VM。
|
||||
pub title: String,
|
||||
// 该标签是否是此窗口当前活动页;页面注册表顺序与选中状态分别表示。
|
||||
// 投影可反映待提交切页请求,不能仅凭这一字段推断新页面布局已经发布成功。
|
||||
pub active: bool,
|
||||
// 开发工具页的类别标识;普通页省略此字段,反序列化缺失字段时默认为 None。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
||||
Reference in New Issue
Block a user