dsh-notch 0.3.1
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/BASELINE.md +13 -0
- package/DESIGN.md +34 -0
- package/LICENSE +21 -0
- package/README.en.md +136 -0
- package/README.md +145 -0
- package/cordis.yml +5 -0
- package/docs/motion-audit.md +65 -0
- package/docs/motion-continuity.md +77 -0
- package/docs/releases/v0.3.0.md +20 -0
- package/dshx.yml +14 -0
- package/macos/Package.swift +17 -0
- package/macos/STATUS-MOTION.md +49 -0
- package/macos/Sources/Client.swift +139 -0
- package/macos/Sources/IdleRobot.swift +423 -0
- package/macos/Sources/NotchMarkdown.swift +266 -0
- package/macos/Sources/Panel.swift +153 -0
- package/macos/Sources/Resources/Idle/balance.json +1 -0
- package/macos/Sources/Resources/Idle/blink.json +1 -0
- package/macos/Sources/Resources/Idle/cube-in.json +1 -0
- package/macos/Sources/Resources/Idle/dance.json +1 -0
- package/macos/Sources/Resources/Idle/hop.json +1 -0
- package/macos/Sources/Resources/Idle/nod.json +1 -0
- package/macos/Sources/Resources/Idle/satellite-in.json +1 -0
- package/macos/Sources/Resources/Idle/satellite-out.json +1 -0
- package/macos/Sources/Resources/Idle/scan.json +1 -0
- package/macos/Sources/Resources/Idle/sleep.json +1 -0
- package/macos/Sources/Resources/Idle/sneeze.json +1 -0
- package/macos/Sources/Resources/Idle/stretch.json +1 -0
- package/macos/Sources/Resources/Idle/tilt.json +1 -0
- package/macos/Sources/ReviewStudio.swift +337 -0
- package/macos/Sources/RootView.swift +1093 -0
- package/macos/Sources/StatusOrbit.swift +642 -0
- package/macos/Sources/main.swift +153 -0
- package/macos/Tests/ExpandedHeightProbe.swift +108 -0
- package/macos/Tests/GeometryProbe.swift +98 -0
- package/macos/Tests/IdleProbe.swift +294 -0
- package/macos/Tests/MotionProbe.swift +106 -0
- package/macos/Tests/OutcomeProbe.swift +133 -0
- package/macos/Tests/ScrollbarProbe.swift +91 -0
- package/macos/Tests/expanded-height.sh +13 -0
- package/macos/Tests/geometry.sh +14 -0
- package/macos/Tests/idle.sh +14 -0
- package/macos/Tests/motion.sh +14 -0
- package/macos/Tests/outcome.sh +14 -0
- package/macos/Tests/scrollbar.sh +13 -0
- package/package.json +46 -0
- package/src/board.ts +299 -0
- package/src/browse-sync.ts +59 -0
- package/src/dsh-notch.ts +60 -0
- package/src/http.ts +215 -0
- package/src/session-state.ts +31 -0
- package/src/store.ts +49 -0
- package/src/types.ts +65 -0
- package/tests/answer-lifecycle.test.mjs +23 -0
- package/tests/board.test.mjs +7 -0
- package/tests/browse-sync.test.mjs +20 -0
- package/tests/mirror.test.mjs +4 -0
- package/tests/peer-range.test.mjs +21 -0
- package/tests/session-state.test.mjs +54 -0
- package/tests/unread-hold.test.mjs +90 -0
- package/tools/desktop-shell/README.md +56 -0
- package/tools/desktop-shell/child-log.mjs +6 -0
- package/tools/desktop-shell/child-log.test.mjs +14 -0
- package/tools/desktop-shell/package-recovery.py +78 -0
- package/tools/desktop-shell/recovery-probe.cjs +50 -0
- package/tools/desktop-shell/renderer-recovery.mjs +95 -0
- package/tools/desktop-shell/renderer-recovery.test.mjs +24 -0
- package/tools/diagnostics/completion-watch.py +101 -0
- package/tools/diagnostics/completion-watch.test.py +17 -0
- package/tools/diagnostics/renderer-diagnostics.mjs +56 -0
- package/tools/diagnostics/renderer-diagnostics.test.mjs +32 -0
- package/tools/idle/LICENSE.open-bot-motion +21 -0
- package/tools/idle/export.mjs +40 -0
- package/tools/idle/index.html +4 -0
- package/tools/idle/motions.js +83 -0
- package/tools/idle/original.js +1673 -0
- package/tools/idle/renderer.js +1673 -0
- package/tools/recording/Client.swift +76 -0
- package/tools/recording/DemoCatalog.swift +174 -0
- package/tools/recording/README.md +23 -0
- package/tools/recording/build.sh +41 -0
- package/tools/recording/main.swift +100 -0
- package/tools/review/ReviewStudio.swift +236 -0
- package/tools/review/audit.sh +19 -0
- package/tools/review/build.sh +7 -0
- package/tools/review/main.swift +158 -0
- package/tools/review/render-audit.swift +32 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct RuntimeFile: Decodable {
|
|
4
|
+
var origin: String
|
|
5
|
+
var token: String
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
struct NotchOption: Decodable, Identifiable {
|
|
9
|
+
var label: String
|
|
10
|
+
var description: String?
|
|
11
|
+
var id: String { label }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
struct NotchQuestion: Decodable, Identifiable {
|
|
15
|
+
var id: String
|
|
16
|
+
var question: String
|
|
17
|
+
var detail: String?
|
|
18
|
+
var header: String?
|
|
19
|
+
var options: [NotchOption]?
|
|
20
|
+
var multiSelect: Bool?
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
struct NotchApproval: Decodable {
|
|
24
|
+
var id: String
|
|
25
|
+
var toolName: String
|
|
26
|
+
var reason: String?
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
struct NotchAsk: Decodable {
|
|
30
|
+
var id: String
|
|
31
|
+
var questions: [NotchQuestion]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
struct NotchLastTurn: Decodable {
|
|
35
|
+
var at: Double
|
|
36
|
+
var kind: String
|
|
37
|
+
var failed: Bool
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
struct NotchRow: Decodable, Identifiable {
|
|
41
|
+
var id: String
|
|
42
|
+
var title: String
|
|
43
|
+
var child: Bool
|
|
44
|
+
var busy: Bool
|
|
45
|
+
var unread: Bool
|
|
46
|
+
var lastTurn: NotchLastTurn?
|
|
47
|
+
var approval: NotchApproval?
|
|
48
|
+
var ask: NotchAsk?
|
|
49
|
+
|
|
50
|
+
var needsAction: Bool { approval != nil || ask != nil }
|
|
51
|
+
/// A red lamp is a finished unsuccessful turn, never a session that is still running.
|
|
52
|
+
var isFailedResult: Bool { lastTurn?.failed == true && !busy && !needsAction }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
struct NotchSnapshot: Decodable {
|
|
56
|
+
var ok: Bool
|
|
57
|
+
var generatedAt: Double
|
|
58
|
+
var origin: String
|
|
59
|
+
var rows: [NotchRow]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
enum NotchClientError: Error {
|
|
63
|
+
case noRuntime
|
|
64
|
+
case http(Int)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Preview-only transport: no credential reads, network, or live session actions.
|
|
68
|
+
final class NotchClient: @unchecked Sendable {
|
|
69
|
+
func reloadRuntime() throws { throw NotchClientError.noRuntime }
|
|
70
|
+
func status() async throws -> NotchSnapshot { throw NotchClientError.noRuntime }
|
|
71
|
+
func approve(id: String, outcome: String) async throws {}
|
|
72
|
+
@MainActor func answer(id: String, answers: [[String: Any]]) async throws {}
|
|
73
|
+
func seen(sessionId: String) async throws {}
|
|
74
|
+
func seenAll() async throws {}
|
|
75
|
+
func focus(sessionId: String) async throws {}
|
|
76
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
final class PreviewIdleCue: @unchecked Sendable {
|
|
5
|
+
static let notification=Notification.Name("NotchRecordingIdleCue")
|
|
6
|
+
let boardID:ObjectIdentifier
|
|
7
|
+
let action:String
|
|
8
|
+
init(board:BoardModel,action:String) {boardID=ObjectIdentifier(board);self.action=action}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
enum DemoState {case running, decision, success, failure}
|
|
12
|
+
struct DemoBeat {
|
|
13
|
+
let at:Double
|
|
14
|
+
var rows:[NotchRow]? = nil
|
|
15
|
+
var action:String? = nil
|
|
16
|
+
}
|
|
17
|
+
struct DemoScene: Identifiable {
|
|
18
|
+
let id:String
|
|
19
|
+
let title:String
|
|
20
|
+
var englishTitle:String {DemoCatalog.english[id] ?? id}
|
|
21
|
+
let initial:[NotchRow]
|
|
22
|
+
let beats:[DemoBeat]
|
|
23
|
+
var duration:Double=7.8
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
enum DemoCatalog {
|
|
27
|
+
static let english:[String:String] = [
|
|
28
|
+
"birth-blue":"Idle → Running",
|
|
29
|
+
"birth-yellow":"Idle → Decision",
|
|
30
|
+
"one-yellow":"Running → Decision",
|
|
31
|
+
"one-return":"Decision → Running",
|
|
32
|
+
"one-green":"Running → Done",
|
|
33
|
+
"one-red":"Running → Failed",
|
|
34
|
+
"two-yellow":"Two Tasks → Decision",
|
|
35
|
+
"two-return":"Decision → Two Tasks",
|
|
36
|
+
"two-green":"Two Tasks → One Done",
|
|
37
|
+
"two-red":"Two Tasks → One Failed",
|
|
38
|
+
"one-roundtrip":"One Task · Decision & Resume",
|
|
39
|
+
"two-roundtrip":"Two Tasks · Decision & Resume",
|
|
40
|
+
"green-idle":"Done → Idle",
|
|
41
|
+
"red-idle":"Failed → Idle",
|
|
42
|
+
"green-blue":"Read Result → Keep Running",
|
|
43
|
+
"red-blue":"Read Failure → Keep Running",
|
|
44
|
+
"clear-green":"Read Result → Keep Failure",
|
|
45
|
+
"clear-red":"Read Failure → Keep Result",
|
|
46
|
+
"add-work":"One Task → Many Tasks",
|
|
47
|
+
"all-green":"More Tasks Done",
|
|
48
|
+
"all-red":"More Tasks Failed",
|
|
49
|
+
"mixed-green":"Last Task → Done",
|
|
50
|
+
"mixed-red":"Last Task → Failed",
|
|
51
|
+
"clear-all":"All Read → Idle",
|
|
52
|
+
"idle-blink":"Blink",
|
|
53
|
+
"idle-scan":"Look Around",
|
|
54
|
+
"idle-tilt":"Head Tilt",
|
|
55
|
+
"idle-nod":"Nod",
|
|
56
|
+
"idle-stretch":"Stretch",
|
|
57
|
+
"idle-hop":"Hop",
|
|
58
|
+
"idle-balance":"Balance",
|
|
59
|
+
"idle-sneeze":"Sneeze",
|
|
60
|
+
"idle-sleep":"Doze",
|
|
61
|
+
"idle-dance":"Chameleon",
|
|
62
|
+
"idle-flow":"Motion → Motion",
|
|
63
|
+
"dance-flow":"Chameleon Transitions",
|
|
64
|
+
]
|
|
65
|
+
static func rows(_ states:[DemoState])->[NotchRow] {
|
|
66
|
+
states.enumerated().map {index,state in
|
|
67
|
+
var row=NotchRow(id:"demo-task-\(index)",title:"本地任务",child:false,busy:state == .running || state == .decision,unread:state == .success || state == .failure)
|
|
68
|
+
if state == .decision {row.approval=NotchApproval(id:"demo-approval-\(index)",toolName:"本地演示")}
|
|
69
|
+
if state == .success || state == .failure {row.lastTurn=NotchLastTurn(at:1,kind:"demo",failed:state == .failure)}
|
|
70
|
+
return row
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
static func scene(_ id:String,_ title:String,_ from:[DemoState],_ to:[DemoState],at:Double=2.3)->DemoScene {
|
|
74
|
+
DemoScene(id:id,title:title,initial:rows(from),beats:[DemoBeat(at:at,rows:rows(to))])
|
|
75
|
+
}
|
|
76
|
+
static func clear(_ id:String,_ title:String,_ from:[DemoState],index:Int)->DemoScene {
|
|
77
|
+
let source=rows(from)
|
|
78
|
+
return DemoScene(id:id,title:title,initial:source,beats:[DemoBeat(at:2.3,rows:source.filter {$0.id != "demo-task-\(index)"})])
|
|
79
|
+
}
|
|
80
|
+
static func idle(_ action:String,_ title:String)->DemoScene {
|
|
81
|
+
DemoScene(id:"idle-\(action)",title:title,initial:[],beats:[DemoBeat(at:0.65,action:action)],duration:action == "sleep" ? 10.5:9)
|
|
82
|
+
}
|
|
83
|
+
static let groups:[[DemoScene]] = [
|
|
84
|
+
[scene("birth-blue","待机 → 运行",[],[.running]),
|
|
85
|
+
scene("birth-yellow","待机 → 决策",[],[.decision]),
|
|
86
|
+
scene("one-yellow","运行 → 决策",[.running],[.decision]),
|
|
87
|
+
scene("one-return","决策 → 运行",[.decision],[.running]),
|
|
88
|
+
scene("one-green","运行 → 完成",[.running],[.success]),
|
|
89
|
+
scene("one-red","运行 → 失败",[.running],[.failure])],
|
|
90
|
+
[scene("two-yellow","两个任务 → 决策",[.running,.running],[.decision,.running]),
|
|
91
|
+
scene("two-return","决策 → 两个任务",[.decision,.running],[.running,.running]),
|
|
92
|
+
scene("two-green","两个任务 → 完成",[.running,.running],[.success,.running]),
|
|
93
|
+
scene("two-red","两个任务 → 失败",[.running,.running],[.failure,.running]),
|
|
94
|
+
DemoScene(id:"one-roundtrip",title:"一个任务 · 决策往返",initial:rows([.running]),beats:[DemoBeat(at:1.6,rows:rows([.decision])),DemoBeat(at:4.5,rows:rows([.running]))]),
|
|
95
|
+
DemoScene(id:"two-roundtrip",title:"两个任务 · 决策往返",initial:rows([.running,.running]),beats:[DemoBeat(at:1.6,rows:rows([.decision,.running])),DemoBeat(at:4.5,rows:rows([.running,.running]))])],
|
|
96
|
+
[clear("green-idle","完成 → 待机",[.success],index:0),
|
|
97
|
+
clear("red-idle","失败 → 待机",[.failure],index:0),
|
|
98
|
+
clear("green-blue","已读完成 → 继续运行",[.success,.running],index:0),
|
|
99
|
+
clear("red-blue","已读失败 → 继续运行",[.failure,.running],index:0),
|
|
100
|
+
clear("clear-green","已读完成 → 保留失败",[.success,.failure],index:0),
|
|
101
|
+
clear("clear-red","已读失败 → 保留完成",[.success,.failure],index:1)],
|
|
102
|
+
[scene("add-work","一个任务 → 多个任务",[.running],[.running,.running,.running]),
|
|
103
|
+
scene("all-green","继续完成 → 完成累积",[.success,.running],[.success,.success]),
|
|
104
|
+
scene("all-red","继续失败 → 失败累积",[.failure,.running],[.failure,.failure]),
|
|
105
|
+
scene("mixed-green","最后任务 → 完成",[.success,.failure,.running],[.success,.failure,.success]),
|
|
106
|
+
scene("mixed-red","最后任务 → 失败",[.success,.failure,.running],[.success,.failure,.failure]),
|
|
107
|
+
DemoScene(id:"clear-all",title:"全部已读 → 待机",initial:rows([.success,.failure]),beats:[DemoBeat(at:1.6,rows:rows([.success,.failure]).filter {$0.id != "demo-task-0"}),DemoBeat(at:4.1,rows:[])])],
|
|
108
|
+
[idle("blink","眨眼"),idle("scan","左右看看"),idle("tilt","歪头"),idle("nod","点头"),idle("stretch","伸懒腰"),idle("hop","蹦一蹦")],
|
|
109
|
+
[idle("balance","保持平衡"),idle("sneeze","打喷嚏"),idle("sleep","打个盹"),idle("dance","变色龙"),
|
|
110
|
+
DemoScene(id:"idle-flow",title:"动作自然衔接",initial:[],beats:[DemoBeat(at:0.65,action:"nod"),DemoBeat(at:4.4,action:"stretch")],duration:12),
|
|
111
|
+
DemoScene(id:"dance-flow",title:"彩蛋自然衔接",initial:[],beats:[DemoBeat(at:0.65,action:"dance"),DemoBeat(at:5.3,action:"scan")],duration:13)]
|
|
112
|
+
]
|
|
113
|
+
static var all:[DemoScene] {groups.flatMap {$0}}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@MainActor final class DemoTile:ObservableObject,Identifiable {
|
|
117
|
+
let id=UUID()
|
|
118
|
+
let scene:DemoScene
|
|
119
|
+
let board=BoardModel()
|
|
120
|
+
init(_ scene:DemoScene) {
|
|
121
|
+
self.scene=scene;board.previewMode=true
|
|
122
|
+
apply(scene.initial);board.tickOrbitLayout(at:Date().addingTimeInterval(3))
|
|
123
|
+
}
|
|
124
|
+
func apply(_ rows:[NotchRow]) {
|
|
125
|
+
board.applySnapshot(NotchSnapshot(ok:true,generatedAt:0,origin:"offline-recording",rows:rows))
|
|
126
|
+
}
|
|
127
|
+
func apply(_ beat:DemoBeat) {
|
|
128
|
+
if let rows=beat.rows {apply(rows)}
|
|
129
|
+
if let action=beat.action {NotificationCenter.default.post(name:PreviewIdleCue.notification,object:PreviewIdleCue(board:board,action:action))}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@MainActor final class RecordingModel:ObservableObject {
|
|
134
|
+
@Published var tiles:[DemoTile]=[]
|
|
135
|
+
@Published var group=0
|
|
136
|
+
@Published var selected=0
|
|
137
|
+
@Published var solo=false
|
|
138
|
+
@Published var autoplay=true
|
|
139
|
+
@Published var controls=false
|
|
140
|
+
private var job:Task<Void,Never>?
|
|
141
|
+
private var epoch=0
|
|
142
|
+
var count:Int {solo ? DemoCatalog.all.count:DemoCatalog.groups.count}
|
|
143
|
+
var page:Int {solo ? selected:group}
|
|
144
|
+
var scenes:[DemoScene] {solo ? [DemoCatalog.all[selected]]:DemoCatalog.groups[group]}
|
|
145
|
+
func move(_ direction:Int) {
|
|
146
|
+
if solo {selected=(selected+direction+count)%count} else {group=(group+direction+count)%count}
|
|
147
|
+
replay()
|
|
148
|
+
}
|
|
149
|
+
func toggleLayout() {
|
|
150
|
+
if !solo {selected=group*6} else {group=selected/6}
|
|
151
|
+
solo.toggle();replay()
|
|
152
|
+
}
|
|
153
|
+
func replay() {
|
|
154
|
+
job?.cancel();epoch += 1;let token=epoch
|
|
155
|
+
job=Task { @MainActor in
|
|
156
|
+
while !Task.isCancelled,token==epoch {
|
|
157
|
+
let batch=scenes.map(DemoTile.init);tiles=batch
|
|
158
|
+
let duration=scenes.map(\.duration).max() ?? 8
|
|
159
|
+
let events=batch.flatMap {tile in tile.scene.beats.map {(tile,$0)}}.sorted {$0.1.at < $1.1.at}
|
|
160
|
+
var last=0.0
|
|
161
|
+
for (tile,beat) in events {
|
|
162
|
+
do {try await Task.sleep(for:.seconds(max(0,beat.at-last)))} catch {return}
|
|
163
|
+
guard token==epoch else {return};tile.apply(beat);last=beat.at
|
|
164
|
+
}
|
|
165
|
+
do {try await Task.sleep(for:.seconds(max(0,duration-last)))} catch {return}
|
|
166
|
+
guard token==epoch else {return}
|
|
167
|
+
if autoplay {
|
|
168
|
+
if solo {selected=(selected+1)%count} else {group=(group+1)%count}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
func stop() {job?.cancel();job=nil;epoch += 1}
|
|
174
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Bilingual recording demo / 双语录屏演示
|
|
2
|
+
|
|
3
|
+
Build from the current production animation sources:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm run build:demo
|
|
7
|
+
open "dist/DSH Notch Demo.app"
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
36 scenes in six groups cover idle, working, decisions and resume, success, failure, unread clearing, nine robot motions, and the chameleon easter egg. Chinese and English titles share one fixed title area. The demo uses local fixtures and a transport stub; it does not connect to DSH or use model tokens.
|
|
11
|
+
|
|
12
|
+
共 6 组、36 个场景。中文在上、英文在下,仅保留状态名称。动画源与正式 helper 相同,演示仅用本地假任务驱动,不连接 DSH。
|
|
13
|
+
|
|
14
|
+
| Key | Action / 操作 |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| ← / → | Previous / next group · 上一组 / 下一组 |
|
|
17
|
+
| R | Replay · 重播 |
|
|
18
|
+
| S | Grid / single scene · 并排 / 单场景 |
|
|
19
|
+
| H | Show / hide controls · 显示 / 隐藏控制栏 |
|
|
20
|
+
| Control + Command + F | Full screen · 全屏 |
|
|
21
|
+
| Command + Q | Quit · 退出 |
|
|
22
|
+
|
|
23
|
+
Disable “全部连播” to keep looping the selected group. Move the pointer away and hide the controls before recording. The regular helper and DSH can remain running.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
repo=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
|
4
|
+
export DEVELOPER_DIR="${DEVELOPER_DIR:-/Library/Developer/CommandLineTools}"
|
|
5
|
+
work="$repo/.build/recording"
|
|
6
|
+
mkdir -p "$work/Sources" "$repo/dist"
|
|
7
|
+
python3 - "$repo" "$work" <<'PY'
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import shutil,sys
|
|
10
|
+
repo,work=map(Path,sys.argv[1:])
|
|
11
|
+
for name in ['RootView.swift','StatusOrbit.swift','IdleRobot.swift','Panel.swift','NotchMarkdown.swift']:
|
|
12
|
+
shutil.copy2(repo/'macos/Sources'/name,work/'Sources'/name)
|
|
13
|
+
for name in ['DemoCatalog.swift','main.swift','Client.swift']:
|
|
14
|
+
shutil.copy2(repo/'tools/recording'/name,work/'Sources'/name)
|
|
15
|
+
shutil.copytree(repo/'macos/Sources/Resources',work/'Sources/Resources',dirs_exist_ok=True)
|
|
16
|
+
p=work/'Sources/IdleRobot.swift';s=p.read_text()
|
|
17
|
+
old='.onAppear { presence.set(idle, director: director, animated: false) }'
|
|
18
|
+
new='''.onAppear { director.automaticActions=false;presence.set(idle, director: director, animated: false) }
|
|
19
|
+
.onReceive(NotificationCenter.default.publisher(for:PreviewIdleCue.notification)) { note in
|
|
20
|
+
guard let cue=note.object as? PreviewIdleCue,cue.boardID==ObjectIdentifier(model) else {return}
|
|
21
|
+
director.automaticActions=false;director.play(cue.action)
|
|
22
|
+
}'''
|
|
23
|
+
assert s.count(old)==1,'Idle playback hook changed; inspect the production source before updating this adapter.'
|
|
24
|
+
p.write_text(s.replace(old,new))
|
|
25
|
+
(work/'Package.swift').write_text('''// swift-tools-version: 6.0
|
|
26
|
+
import PackageDescription
|
|
27
|
+
let package = Package(name:"NotchDemo",platforms:[.macOS(.v14)],products:[.executable(name:"NotchDemo",targets:["NotchDemo"])],targets:[.executableTarget(name:"NotchDemo",path:"Sources",resources:[.copy("Resources/Idle")])])
|
|
28
|
+
''')
|
|
29
|
+
PY
|
|
30
|
+
swift build --package-path "$work" -c release
|
|
31
|
+
python3 - "$repo" "$work" <<'PY'
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
import shutil,plistlib,sys
|
|
34
|
+
repo,work=map(Path,sys.argv[1:]);app=repo/'dist/DSH Notch Demo.app/Contents'
|
|
35
|
+
(app/'MacOS').mkdir(parents=True,exist_ok=True);(app/'Resources').mkdir(exist_ok=True)
|
|
36
|
+
shutil.copy2(work/'.build/release/NotchDemo',app/'MacOS/NotchDemo')
|
|
37
|
+
for bundle in (work/'.build/release').glob('*.bundle'):
|
|
38
|
+
shutil.copytree(bundle,app/'Resources'/bundle.name,dirs_exist_ok=True)
|
|
39
|
+
(app/'Info.plist').write_bytes(plistlib.dumps({'CFBundleIdentifier':'local.dsh.notch.demo','CFBundleName':'DSH Notch Demo','CFBundleExecutable':'NotchDemo','CFBundlePackageType':'APPL','CFBundleShortVersionString':'0.3.0','CFBundleVersion':'3','NSHighResolutionCapable':True,'LSMinimumSystemVersion':'14.0'}))
|
|
40
|
+
print(app.parent)
|
|
41
|
+
PY
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
struct RecordingTileView:View {
|
|
5
|
+
let tile:DemoTile
|
|
6
|
+
let solo:Bool
|
|
7
|
+
@ObservedObject var board:BoardModel
|
|
8
|
+
init(tile:DemoTile,solo:Bool) {self.tile=tile;self.solo=solo;board=tile.board}
|
|
9
|
+
var body:some View {
|
|
10
|
+
GeometryReader {geo in
|
|
11
|
+
let scale:CGFloat=solo ? min(5.4,(geo.size.height-140)/110):min(2.65,(geo.size.height-104)/104)
|
|
12
|
+
VStack(spacing:0) {
|
|
13
|
+
VStack(spacing:5) {
|
|
14
|
+
Text(tile.scene.title).font(.system(size:solo ? 40:24,weight:.semibold))
|
|
15
|
+
.foregroundStyle(Color(white:0.14))
|
|
16
|
+
Text(tile.scene.englishTitle).font(.system(size:solo ? 27:17,weight:.medium))
|
|
17
|
+
.foregroundStyle(Color(white:0.34))
|
|
18
|
+
}.lineLimit(1).minimumScaleFactor(0.7)
|
|
19
|
+
.frame(height:solo ? 130:82)
|
|
20
|
+
ZStack {
|
|
21
|
+
RootView(model:board,panelSize:CGSize(width:340,height:260),restSize:CGSize(width:38,height:44))
|
|
22
|
+
.frame(width:38,height:max(44,board.orbitLayout.height+24))
|
|
23
|
+
.scaleEffect(scale).allowsHitTesting(false)
|
|
24
|
+
}.frame(maxWidth:.infinity,maxHeight:.infinity)
|
|
25
|
+
}.frame(width:geo.size.width,height:geo.size.height)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
struct RecordingView:View {
|
|
31
|
+
@ObservedObject var model:RecordingModel
|
|
32
|
+
var body:some View {
|
|
33
|
+
GeometryReader {geo in
|
|
34
|
+
ZStack(alignment:.bottom) {
|
|
35
|
+
Color(white:0.965)
|
|
36
|
+
if model.solo {
|
|
37
|
+
if let tile=model.tiles.first {
|
|
38
|
+
RecordingTileView(tile:tile,solo:true).id(tile.id).padding(.horizontal,60).padding(.top,22).padding(.bottom,80)
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
VStack(spacing:16) {
|
|
42
|
+
ForEach(0..<2,id:\.self) {row in
|
|
43
|
+
HStack(spacing:26) {
|
|
44
|
+
ForEach(Array(model.tiles.dropFirst(row*3).prefix(3))) {tile in
|
|
45
|
+
RecordingTileView(tile:tile,solo:false).id(tile.id)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}.padding(.horizontal,38).padding(.top,16).padding(.bottom,68)
|
|
50
|
+
}
|
|
51
|
+
HStack(spacing:18) {
|
|
52
|
+
Button {model.move(-1)} label:{Image(systemName:"chevron.left")}.keyboardShortcut(.leftArrow,modifiers:[]).help("上一组")
|
|
53
|
+
Text("\(model.page+1) / \(model.count)").monospacedDigit().foregroundStyle(.secondary)
|
|
54
|
+
Button {model.move(1)} label:{Image(systemName:"chevron.right")}.keyboardShortcut(.rightArrow,modifiers:[]).help("下一组")
|
|
55
|
+
Divider().frame(height:18)
|
|
56
|
+
Button {model.replay()} label:{Image(systemName:"arrow.counterclockwise")}.keyboardShortcut("r",modifiers:[]).help("重播")
|
|
57
|
+
Toggle("全部连播",isOn:$model.autoplay).toggleStyle(.checkbox)
|
|
58
|
+
Button {model.controls.toggle()} label:{Image(systemName:"eye.slash")}.keyboardShortcut("h",modifiers:[]).help("显示 / 隐藏控制栏")
|
|
59
|
+
Button {model.toggleLayout()} label:{Image(systemName:model.solo ? "square.grid.3x2":"rectangle")}.keyboardShortcut("s",modifiers:[]).help("并排 / 单场景")
|
|
60
|
+
Button {NSApp.keyWindow?.toggleFullScreen(nil)} label:{Image(systemName:"arrow.up.left.and.arrow.down.right")}.keyboardShortcut("f",modifiers:[.command,.control]).help("全屏")
|
|
61
|
+
}.font(.system(size:14)).buttonStyle(.plain)
|
|
62
|
+
.padding(.horizontal,22).padding(.vertical,14).background(.regularMaterial,in:Capsule())
|
|
63
|
+
.padding(.bottom,15).opacity(model.controls ? 1:0).allowsHitTesting(model.controls)
|
|
64
|
+
.animation(.easeOut(duration:0.15),value:model.controls)
|
|
65
|
+
}.onContinuousHover {phase in
|
|
66
|
+
switch phase {case .active(let p):model.controls=p.y > geo.size.height-70
|
|
67
|
+
case .ended:model.controls=false}
|
|
68
|
+
}
|
|
69
|
+
}.frame(minWidth:900,minHeight:620)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@MainActor final class RecordingDelegate:NSObject,NSApplicationDelegate,NSWindowDelegate {
|
|
74
|
+
let model=RecordingModel()
|
|
75
|
+
var window:NSWindow?
|
|
76
|
+
func applicationDidFinishLaunching(_ notification:Notification) {
|
|
77
|
+
let window=NSWindow(contentRect:NSRect(x:0,y:0,width:1200,height:780),styleMask:[.titled,.closable,.miniaturizable,.resizable,.fullSizeContentView],backing:.buffered,defer:false)
|
|
78
|
+
window.title="DSH Notch · Demo"
|
|
79
|
+
window.titleVisibility = .hidden;window.titlebarAppearsTransparent=true
|
|
80
|
+
window.contentView=NSHostingView(rootView:RecordingView(model:model))
|
|
81
|
+
window.backgroundColor=NSColor(white:0.965,alpha:1)
|
|
82
|
+
window.isReleasedWhenClosed=false;window.delegate=self
|
|
83
|
+
window.collectionBehavior=[.fullScreenPrimary]
|
|
84
|
+
window.center();window.makeKeyAndOrderFront(nil);self.window=window
|
|
85
|
+
NSApp.activate(ignoringOtherApps:true);model.replay()
|
|
86
|
+
}
|
|
87
|
+
func windowWillClose(_ notification:Notification) {model.stop();NSApp.terminate(nil)}
|
|
88
|
+
func applicationShouldTerminateAfterLastWindowClosed(_ sender:NSApplication)->Bool {true}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@main enum RecordingMain {
|
|
92
|
+
@MainActor static func main() {
|
|
93
|
+
let app=NSApplication.shared;app.setActivationPolicy(.regular)
|
|
94
|
+
let menu=NSMenu(),item=NSMenuItem(),sub=NSMenu()
|
|
95
|
+
sub.addItem(withTitle:"退出演示",action:#selector(NSApplication.terminate(_:)),keyEquivalent:"q")
|
|
96
|
+
item.submenu=sub;menu.addItem(item);app.mainMenu=menu
|
|
97
|
+
let delegate=RecordingDelegate();app.delegate=delegate
|
|
98
|
+
withExtendedLifetime(delegate) {app.run()}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
/// Earlier recording-stage reference. The supported recorder is in tools/recording.
|
|
5
|
+
@MainActor
|
|
6
|
+
final class DemoStudio: ObservableObject {
|
|
7
|
+
@Published var board = BoardModel()
|
|
8
|
+
@Published var caption = "按「开始」再录"
|
|
9
|
+
@Published var playing = false
|
|
10
|
+
private var job: Task<Void, Never>?
|
|
11
|
+
private var generation = 0
|
|
12
|
+
|
|
13
|
+
init() { resetBoard() }
|
|
14
|
+
|
|
15
|
+
func reset() {
|
|
16
|
+
job?.cancel()
|
|
17
|
+
job = nil
|
|
18
|
+
playing = false
|
|
19
|
+
generation += 1
|
|
20
|
+
caption = "按「开始」再录"
|
|
21
|
+
resetBoard()
|
|
22
|
+
IdleDirector.previewInstance?.automaticActions = false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func start() {
|
|
26
|
+
job?.cancel()
|
|
27
|
+
generation += 1
|
|
28
|
+
let token = generation
|
|
29
|
+
resetBoard()
|
|
30
|
+
playing = true
|
|
31
|
+
job = Task { @MainActor in
|
|
32
|
+
await runScript(token)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private func resetBoard() {
|
|
37
|
+
board = BoardModel()
|
|
38
|
+
board.previewMode = true
|
|
39
|
+
board.maximumExpandedHeight = 460
|
|
40
|
+
board.expanded = false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private func snap(_ rows: [NotchRow]) {
|
|
44
|
+
board.applySnapshot(NotchSnapshot(ok: true, generatedAt: Date().timeIntervalSince1970, origin: "demo", rows: rows))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private func busy(_ n: Int) -> [NotchRow] {
|
|
48
|
+
(0..<n).map { i in
|
|
49
|
+
NotchRow(id: "demo-\(i)", title: "任务 \(i + 1)", child: false, busy: true, unread: false, lastTurn: nil, approval: nil, ask: nil)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private func mixed() -> [NotchRow] {
|
|
54
|
+
[
|
|
55
|
+
NotchRow(id: "demo-0", title: "任务 1", child: false, busy: false, unread: true, lastTurn: nil, approval: nil, ask: nil),
|
|
56
|
+
NotchRow(
|
|
57
|
+
id: "demo-1",
|
|
58
|
+
title: "任务 2",
|
|
59
|
+
child: false,
|
|
60
|
+
busy: false,
|
|
61
|
+
unread: true,
|
|
62
|
+
lastTurn: NotchLastTurn(at: Date().timeIntervalSince1970, kind: "error", failed: true),
|
|
63
|
+
approval: nil,
|
|
64
|
+
ask: nil
|
|
65
|
+
),
|
|
66
|
+
NotchRow(
|
|
67
|
+
id: "demo-2",
|
|
68
|
+
title: "任务 3",
|
|
69
|
+
child: false,
|
|
70
|
+
busy: false,
|
|
71
|
+
unread: false,
|
|
72
|
+
lastTurn: nil,
|
|
73
|
+
approval: nil,
|
|
74
|
+
ask: NotchAsk(id: "demo-ask", questions: [
|
|
75
|
+
NotchQuestion(
|
|
76
|
+
id: "q1",
|
|
77
|
+
question: "这条任务下一步怎么走?",
|
|
78
|
+
detail: "录屏用的假问题,点任意一项即可。",
|
|
79
|
+
header: nil,
|
|
80
|
+
options: [
|
|
81
|
+
NotchOption(label: "先改动画", description: "收起和展开再顺一点"),
|
|
82
|
+
NotchOption(label: "先改文案", description: "字幕再短一点"),
|
|
83
|
+
NotchOption(label: "都行", description: "你选就好"),
|
|
84
|
+
],
|
|
85
|
+
multiSelect: false
|
|
86
|
+
)
|
|
87
|
+
])
|
|
88
|
+
),
|
|
89
|
+
NotchRow(id: "demo-3", title: "任务 4", child: false, busy: true, unread: false, lastTurn: nil, approval: nil, ask: nil),
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private func runScript(_ token: Int) async {
|
|
94
|
+
func alive() -> Bool { token == generation && !Task.isCancelled }
|
|
95
|
+
|
|
96
|
+
try? await Task.sleep(for: .milliseconds(400))
|
|
97
|
+
guard alive() else { return }
|
|
98
|
+
IdleDirector.previewInstance?.automaticActions = false
|
|
99
|
+
let tour = IdleDirector.basics + ["dance"]
|
|
100
|
+
for id in tour {
|
|
101
|
+
guard alive() else { return }
|
|
102
|
+
caption = "待机 · \(id)"
|
|
103
|
+
IdleDirector.previewInstance?.play(id)
|
|
104
|
+
let wait = IdleDirector.previewInstance?.currentDuration ?? 7
|
|
105
|
+
try? await Task.sleep(for: .seconds(wait))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
guard alive() else { return }
|
|
109
|
+
caption = "4 个任务开始运行"
|
|
110
|
+
snap(busy(4))
|
|
111
|
+
try? await Task.sleep(for: .seconds(2.2))
|
|
112
|
+
|
|
113
|
+
guard alive() else { return }
|
|
114
|
+
caption = "请把鼠标移到黄点上,点一个选项"
|
|
115
|
+
snap(mixed())
|
|
116
|
+
|
|
117
|
+
while alive() && board.needsAction {
|
|
118
|
+
try? await Task.sleep(for: .milliseconds(120))
|
|
119
|
+
}
|
|
120
|
+
guard alive() else { return }
|
|
121
|
+
caption = "请点红点"
|
|
122
|
+
|
|
123
|
+
while alive() && !board.failedRows.isEmpty {
|
|
124
|
+
try? await Task.sleep(for: .milliseconds(120))
|
|
125
|
+
}
|
|
126
|
+
guard alive() else { return }
|
|
127
|
+
caption = "蓝灯即将结束"
|
|
128
|
+
try? await Task.sleep(for: .seconds(2.6))
|
|
129
|
+
guard alive() else { return }
|
|
130
|
+
snap(board.rows.filter { !$0.busy })
|
|
131
|
+
caption = "请点绿点"
|
|
132
|
+
|
|
133
|
+
while alive() && board.completedUnreadCount > 0 {
|
|
134
|
+
try? await Task.sleep(for: .milliseconds(120))
|
|
135
|
+
}
|
|
136
|
+
guard alive() else { return }
|
|
137
|
+
caption = "结束 · 可按「重来」"
|
|
138
|
+
playing = false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
struct DemoStudioView: View {
|
|
143
|
+
@ObservedObject var studio: DemoStudio
|
|
144
|
+
@State private var hoverNotch = false
|
|
145
|
+
|
|
146
|
+
private var previewWidth: CGFloat {
|
|
147
|
+
studio.board.expanded ? 320 : (studio.board.isPillHovered ? 42 : 38)
|
|
148
|
+
}
|
|
149
|
+
private var previewHeight: CGFloat {
|
|
150
|
+
if studio.board.expanded {
|
|
151
|
+
return min(max(studio.board.measuredContentHeight, studio.board.needsAction ? 280 : 160), 420)
|
|
152
|
+
}
|
|
153
|
+
return max(44, studio.board.orbitLayout.height + 24)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
var body: some View {
|
|
157
|
+
HStack(alignment: .top, spacing: 24) {
|
|
158
|
+
VStack(alignment: .leading, spacing: 12) {
|
|
159
|
+
Text("录屏模拟台").font(.system(size: 18, weight: .semibold))
|
|
160
|
+
Text(studio.caption)
|
|
161
|
+
.font(.system(size: 13))
|
|
162
|
+
.foregroundStyle(.secondary)
|
|
163
|
+
.frame(minHeight: 36, alignment: .topLeading)
|
|
164
|
+
ZStack(alignment: .topTrailing) {
|
|
165
|
+
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
166
|
+
.fill(Color(red: 0.07, green: 0.07, blue: 0.08))
|
|
167
|
+
RootView(
|
|
168
|
+
model: studio.board,
|
|
169
|
+
panelSize: CGSize(width: 320, height: 460),
|
|
170
|
+
restSize: CGSize(width: 38, height: 44)
|
|
171
|
+
)
|
|
172
|
+
.frame(width: previewWidth, height: previewHeight)
|
|
173
|
+
.clipped()
|
|
174
|
+
.onHover { hovering in
|
|
175
|
+
hoverNotch = hovering
|
|
176
|
+
if hovering && studio.board.needsAction {
|
|
177
|
+
studio.board.expanded = true
|
|
178
|
+
} else if !hovering {
|
|
179
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.38) {
|
|
180
|
+
if !hoverNotch { studio.board.expanded = false }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
.frame(width: 360, height: 440, alignment: .topTrailing)
|
|
186
|
+
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
|
|
187
|
+
}
|
|
188
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
189
|
+
Button(studio.playing ? "进行中…" : "开始") { studio.start() }
|
|
190
|
+
.disabled(studio.playing)
|
|
191
|
+
.keyboardShortcut(.defaultAction)
|
|
192
|
+
Button("重来") { studio.reset() }
|
|
193
|
+
Text("不接 Host,不影响正在跑的 notch。\n黄 / 红 / 绿需要你点。")
|
|
194
|
+
.font(.system(size: 11))
|
|
195
|
+
.foregroundStyle(.secondary)
|
|
196
|
+
.padding(.top, 8)
|
|
197
|
+
Spacer()
|
|
198
|
+
}
|
|
199
|
+
.frame(width: 180)
|
|
200
|
+
}
|
|
201
|
+
.padding(24)
|
|
202
|
+
.frame(minWidth: 640, minHeight: 520)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@MainActor
|
|
207
|
+
final class DemoStudioDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
|
208
|
+
let studio = DemoStudio()
|
|
209
|
+
var window: NSWindow?
|
|
210
|
+
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
211
|
+
setbuf(stdout, nil)
|
|
212
|
+
let window = NSPanel(
|
|
213
|
+
contentRect: NSRect(x: 0, y: 0, width: 720, height: 560),
|
|
214
|
+
styleMask: [.titled, .closable, .resizable, .nonactivatingPanel, .hudWindow],
|
|
215
|
+
backing: .buffered,
|
|
216
|
+
defer: false
|
|
217
|
+
)
|
|
218
|
+
window.title = "dsh-notch 录屏模拟台"
|
|
219
|
+
window.delegate = self
|
|
220
|
+
window.isReleasedWhenClosed = false
|
|
221
|
+
window.isFloatingPanel = true
|
|
222
|
+
window.hidesOnDeactivate = false
|
|
223
|
+
window.level = .floating
|
|
224
|
+
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
|
225
|
+
window.contentView = NSHostingView(rootView: DemoStudioView(studio: studio))
|
|
226
|
+
let screen = NSScreen.screens.first { abs($0.frame.minX) < 1 && abs($0.frame.minY) < 1 } ?? NSScreen.main
|
|
227
|
+
if let vis = screen?.visibleFrame {
|
|
228
|
+
window.setFrame(NSRect(x: vis.midX - 360, y: vis.maxY - 620, width: 720, height: 560), display: true)
|
|
229
|
+
}
|
|
230
|
+
window.orderFrontRegardless()
|
|
231
|
+
NSApplication.shared.activate(ignoringOtherApps: true)
|
|
232
|
+
self.window = window
|
|
233
|
+
print("DEMO_STUDIO ready")
|
|
234
|
+
}
|
|
235
|
+
func windowWillClose(_ notification: Notification) { NSApplication.shared.terminate(nil) }
|
|
236
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
repo=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
|
|
4
|
+
work=$(mktemp -d "${TMPDIR:-/tmp}/notch-audit.XXXXXX")
|
|
5
|
+
output=${1:-"$work/frames"}
|
|
6
|
+
mkdir -p "$output"
|
|
7
|
+
cp "$repo/macos/Package.swift" "$work/Package.swift"
|
|
8
|
+
cp -R "$repo/macos/Sources" "$work/Sources"
|
|
9
|
+
python3 - "$repo" "$work" <<'PYTHON'
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import sys
|
|
12
|
+
repo,work=map(Path,sys.argv[1:])
|
|
13
|
+
s=(repo/'tools/review/main.swift').read_text().split('@main enum LoopMain')[0]
|
|
14
|
+
s+=(repo/'tools/review/render-audit.swift').read_text()
|
|
15
|
+
(work/'Sources/main.swift').write_text(s)
|
|
16
|
+
p=work/'Sources/IdleRobot.swift';s=p.read_text().replace('final class IdleDirector: ObservableObject {','final class IdleDirector: ObservableObject {\n static weak var previewInstance: IdleDirector?').replace(' func start(playImmediately: Bool = true) {',' func start(playImmediately: Bool = true) {\n Self.previewInstance=self');p.write_text(s)
|
|
17
|
+
PYTHON
|
|
18
|
+
swift build --package-path "$work" -c release -j 2
|
|
19
|
+
NOTCH_AUDIT_OUTPUT="$output" "$work/.build/release/dsh-notch"
|