azure-pipelines-tui 0.5.0

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.
@@ -0,0 +1,408 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PipelineRunScreen = void 0;
37
+ const blessed = __importStar(require("blessed"));
38
+ const api_js_1 = require("../lib/api.js");
39
+ const format_js_1 = require("../lib/format.js");
40
+ const signalr_js_1 = require("../signalr.js");
41
+ class PipelineRunScreen {
42
+ screen;
43
+ ctx;
44
+ treeWidget;
45
+ logWidget;
46
+ // Build state
47
+ buildId = null;
48
+ build = null;
49
+ records = [];
50
+ logCache = new Map();
51
+ collapsed = new Set();
52
+ expandedGroups = new Set();
53
+ treeItems = [];
54
+ selectedLogId = null;
55
+ followLog = true;
56
+ keepTimestamps;
57
+ // Spinner
58
+ spinnerTimer = null;
59
+ spinnerFrame = 0;
60
+ SPINNER_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
61
+ // SignalR
62
+ signalRHandle = null;
63
+ signalRStarted = false;
64
+ signalRDelay = 3_000;
65
+ pendingLines = new Map();
66
+ // Polling
67
+ pollGen = 0;
68
+ get footerText() {
69
+ const live = this.followLog && this.selectedLogId ? " {green-fg}● LIVE{/}" : "";
70
+ return (" {cyan-fg}↑↓{/} Navigate {cyan-fg}Enter/→{/} Select {cyan-fg}←/Esc{/} Back " +
71
+ `{cyan-fg}Tab{/} Switch {cyan-fg}f{/} Follow {cyan-fg}r{/} Retry {cyan-fg}q{/} Quit${live}`);
72
+ }
73
+ constructor(screen, ctx, opts = {}) {
74
+ this.screen = screen;
75
+ this.ctx = ctx;
76
+ this.keepTimestamps = opts.keepTimestamps ?? false;
77
+ this.treeWidget = blessed.list({
78
+ parent: screen, top: 1, left: 0, width: "34%", height: "100%-3",
79
+ border: { type: "line" }, label: " Pipeline ",
80
+ tags: true, keys: true, vi: true, scrollable: true,
81
+ scrollbar: { ch: "│", style: { fg: "blue" } },
82
+ style: {
83
+ border: { fg: "cyan" }, selected: { bg: "blue", fg: "white", bold: true },
84
+ item: { fg: "white" }, focus: { border: { fg: "white" } },
85
+ },
86
+ items: [], hidden: true,
87
+ });
88
+ this.logWidget = blessed.log({
89
+ parent: screen, top: 1, left: "34%", width: "66%", height: "100%-3",
90
+ border: { type: "line" }, label: " Logs — select a task in the tree ",
91
+ tags: true, keys: true, vi: true, scrollable: true, alwaysScroll: true,
92
+ scrollbar: { ch: "│", style: { fg: "blue" } },
93
+ style: { border: { fg: "gray" }, focus: { border: { fg: "white" } } },
94
+ hidden: true,
95
+ });
96
+ this.registerKeys();
97
+ }
98
+ show(buildId) {
99
+ this.buildId = buildId;
100
+ this.build = null;
101
+ this.records = [];
102
+ this.logCache.clear();
103
+ this.collapsed.clear();
104
+ this.expandedGroups.clear();
105
+ this.treeItems = [];
106
+ this.selectedLogId = null;
107
+ this.followLog = true;
108
+ this.pendingLines.clear();
109
+ this.treeWidget.show();
110
+ this.logWidget.show();
111
+ this.treeWidget.setItems([]);
112
+ this.logWidget.setContent("");
113
+ this.logWidget.setLabel(" Logs — select a task in the tree ");
114
+ this.treeWidget.focus();
115
+ this.screen.render();
116
+ const gen = ++this.pollGen;
117
+ this.poll(buildId, gen);
118
+ }
119
+ hide() {
120
+ this.pollGen++; // stop active poll loop
121
+ this.treeWidget.hide();
122
+ this.logWidget.hide();
123
+ }
124
+ getBuild() { return this.build; }
125
+ getBuildId() { return this.buildId; }
126
+ refreshTree() {
127
+ const sel = this.treeWidget.selected ?? 0;
128
+ const prevId = this.treeItems[sel]
129
+ ? (this.treeItems[sel].kind === "group" ? this.treeItems[sel].id : this.treeItems[sel].record.id)
130
+ : undefined;
131
+ this.treeItems = (0, format_js_1.buildFlatRunTree)(this.records, this.collapsed, this.expandedGroups);
132
+ this.treeWidget.setItems(this.treeItems.map(format_js_1.runItemLabel));
133
+ if (prevId) {
134
+ const idx = this.treeItems.findIndex(t => t.kind === "group" ? t.id === prevId : t.record.id === prevId);
135
+ if (idx >= 0)
136
+ this.treeWidget.select(idx);
137
+ }
138
+ this.screen.render();
139
+ }
140
+ startSpinner(label) {
141
+ if (this.spinnerTimer)
142
+ clearInterval(this.spinnerTimer);
143
+ this.spinnerFrame = 0;
144
+ this.spinnerTimer = setInterval(() => {
145
+ const ch = this.SPINNER_FRAMES[this.spinnerFrame++ % this.SPINNER_FRAMES.length];
146
+ this.logWidget.setLabel(` ${label} {yellow-fg}${ch}{/} `);
147
+ this.screen.render();
148
+ }, 80);
149
+ }
150
+ stopSpinner(label) {
151
+ if (this.spinnerTimer) {
152
+ clearInterval(this.spinnerTimer);
153
+ this.spinnerTimer = null;
154
+ }
155
+ this.logWidget.setLabel(` ${label} `);
156
+ this.screen.render();
157
+ }
158
+ selectLog(id) {
159
+ this.selectedLogId = id;
160
+ const rec = this.records.find(r => r.id === id);
161
+ const name = rec?.name ?? "Logs";
162
+ this.logWidget.setContent("");
163
+ const srLines = this.pendingLines.get(id) ?? [];
164
+ const restLines = rec?.log?.id ? (this.logCache.get(rec.log.id) ?? []) : [];
165
+ const allLines = [...srLines, ...restLines];
166
+ if (allLines.length > 0) {
167
+ this.stopSpinner(name);
168
+ for (const l of allLines)
169
+ this.logWidget.log((0, format_js_1.formatLogLine)(l, this.keepTimestamps));
170
+ this.followLog = true;
171
+ this.logWidget.setScrollPerc(100);
172
+ }
173
+ else {
174
+ this.startSpinner(name);
175
+ }
176
+ this.screen.render();
177
+ }
178
+ appendLines(logId, lines, recordName) {
179
+ const existing = this.logCache.get(logId) ?? [];
180
+ this.logCache.set(logId, [...existing, ...lines]);
181
+ if (this.selectedLogId) {
182
+ const rec = this.records.find(r => r.id === this.selectedLogId);
183
+ if (rec?.log?.id === logId) {
184
+ if (this.spinnerTimer)
185
+ this.stopSpinner(recordName);
186
+ for (const l of lines)
187
+ this.logWidget.log((0, format_js_1.formatLogLine)(l, this.keepTimestamps));
188
+ if (this.followLog)
189
+ this.logWidget.setScrollPerc(100);
190
+ this.screen.render();
191
+ }
192
+ }
193
+ }
194
+ handleSignalR(event) {
195
+ const { method, args } = event;
196
+ if (["BuildUpdated", "TimelineUpdated", "TimelineRecordsUpdated", "timelineRecordsUpdated",
197
+ "JobAssigned", "JobStarted", "JobCompleted"].includes(method)) {
198
+ if (this.buildId)
199
+ this.poll(this.buildId, this.pollGen);
200
+ return;
201
+ }
202
+ if (method === "logConsoleLines") {
203
+ const payload = args[0];
204
+ const lines = payload?.lines;
205
+ const recordId = payload?.stepRecordId;
206
+ if (!lines?.length || !recordId)
207
+ return;
208
+ const rec = this.records.find(r => r.id === recordId);
209
+ if (rec?.log?.id != null) {
210
+ this.appendLines(rec.log.id, lines, rec.name);
211
+ }
212
+ else {
213
+ const prev = this.pendingLines.get(recordId) ?? [];
214
+ this.pendingLines.set(recordId, [...prev, ...lines]);
215
+ if (this.selectedLogId === recordId) {
216
+ const name = rec?.name ?? recordId;
217
+ if (this.spinnerTimer)
218
+ this.stopSpinner(name);
219
+ for (const l of lines)
220
+ this.logWidget.log((0, format_js_1.formatLogLine)(l, this.keepTimestamps));
221
+ if (this.followLog)
222
+ this.logWidget.setScrollPerc(100);
223
+ this.screen.render();
224
+ }
225
+ }
226
+ }
227
+ }
228
+ async setupSignalR(buildId) {
229
+ if (this.signalRStarted) {
230
+ if (this.signalRHandle) {
231
+ try {
232
+ const token = await this.ctx.getToken();
233
+ const projectId = await (0, api_js_1.fetchProjectId)(this.ctx.org, this.ctx.project, token);
234
+ this.signalRHandle.invoke("builddetailhub", "WatchBuild", projectId, Number(buildId));
235
+ }
236
+ catch { /* non-fatal */ }
237
+ }
238
+ return;
239
+ }
240
+ this.signalRStarted = true;
241
+ try {
242
+ const token = await this.ctx.getToken();
243
+ const projectId = await (0, api_js_1.fetchProjectId)(this.ctx.org, this.ctx.project, token);
244
+ this.signalRHandle = await (0, signalr_js_1.connectSignalR)(this.ctx.org, projectId, token, (e) => this.handleSignalR(e), (msg) => this.ctx.setStatus(msg, 5000), () => {
245
+ this.signalRHandle = null;
246
+ this.signalRStarted = false;
247
+ this.ctx.setStatus(`SignalR: reconnecting in ${this.signalRDelay / 1000}s…`, 0);
248
+ setTimeout(() => { if (this.buildId)
249
+ this.setupSignalR(this.buildId); }, this.signalRDelay);
250
+ this.signalRDelay = Math.min(this.signalRDelay * 2, 30_000);
251
+ });
252
+ this.signalRDelay = 3_000;
253
+ this.signalRHandle.invoke("builddetailhub", "WatchBuild", projectId, Number(buildId));
254
+ }
255
+ catch (e) {
256
+ this.signalRStarted = false;
257
+ this.ctx.setStatus(`SignalR: ${e.message.slice(0, 50)} — retry in ${this.signalRDelay / 1000}s`, 0);
258
+ setTimeout(() => { if (this.buildId)
259
+ this.setupSignalR(this.buildId); }, this.signalRDelay);
260
+ this.signalRDelay = Math.min(this.signalRDelay * 2, 30_000);
261
+ }
262
+ }
263
+ async poll(buildId, gen) {
264
+ if (gen !== this.pollGen)
265
+ return;
266
+ try {
267
+ const token = await this.ctx.getToken();
268
+ const [newBuild, timeline] = await Promise.all([
269
+ (0, api_js_1.fetchBuild)(this.ctx.org, this.ctx.project, buildId, token),
270
+ (0, api_js_1.fetchTimeline)(this.ctx.org, this.ctx.project, buildId, token),
271
+ ]);
272
+ if (gen !== this.pollGen)
273
+ return;
274
+ this.build = newBuild;
275
+ if (timeline?.records) {
276
+ this.records = timeline.records;
277
+ this.refreshTree();
278
+ for (const [stepId, lines] of [...this.pendingLines.entries()]) {
279
+ const rec = this.records.find(r => r.id === stepId);
280
+ if (rec?.log?.id != null) {
281
+ const existing = this.logCache.get(rec.log.id) ?? [];
282
+ this.logCache.set(rec.log.id, [...existing, ...lines]);
283
+ this.pendingLines.delete(stepId);
284
+ }
285
+ }
286
+ }
287
+ if (!this.signalRStarted)
288
+ this.setupSignalR(buildId);
289
+ if (this.selectedLogId) {
290
+ const rec = this.records.find(r => r.id === this.selectedLogId);
291
+ if (rec?.log?.id) {
292
+ const logId = rec.log.id;
293
+ const seen = this.logCache.get(logId)?.length ?? 0;
294
+ const data = await (0, api_js_1.fetchLogLines)(this.ctx.org, this.ctx.project, buildId, logId, seen + 1, token);
295
+ if (data?.value?.length)
296
+ this.appendLines(logId, data.value, rec.name);
297
+ }
298
+ }
299
+ }
300
+ catch (e) {
301
+ if (gen === this.pollGen)
302
+ this.ctx.setStatus(e.message.slice(0, 60), 5000);
303
+ }
304
+ if (gen === this.pollGen)
305
+ setTimeout(() => this.poll(buildId, gen), 1000);
306
+ }
307
+ selectedItem() {
308
+ const idx = this.treeWidget.selected ?? 0;
309
+ return this.treeItems[idx];
310
+ }
311
+ registerKeys() {
312
+ this.treeWidget.key(["enter", "right"], () => {
313
+ const item = this.selectedItem();
314
+ if (!item)
315
+ return;
316
+ if (item.kind === "group") {
317
+ if (this.expandedGroups.has(item.id))
318
+ this.expandedGroups.delete(item.id);
319
+ else
320
+ this.expandedGroups.add(item.id);
321
+ this.refreshTree();
322
+ return;
323
+ }
324
+ if (item.hasChildren) {
325
+ if (this.collapsed.has(item.record.id))
326
+ this.collapsed.delete(item.record.id);
327
+ else
328
+ this.collapsed.add(item.record.id);
329
+ this.refreshTree();
330
+ }
331
+ else {
332
+ this.selectLog(item.record.id);
333
+ }
334
+ });
335
+ this.treeWidget.key("left", () => {
336
+ const item = this.selectedItem();
337
+ if (!item)
338
+ return;
339
+ if (item.kind === "group") {
340
+ if (this.expandedGroups.has(item.id)) {
341
+ this.expandedGroups.delete(item.id);
342
+ this.refreshTree();
343
+ }
344
+ return;
345
+ }
346
+ if (item.hasChildren && !this.collapsed.has(item.record.id)) {
347
+ this.collapsed.add(item.record.id);
348
+ this.refreshTree();
349
+ }
350
+ else if (item.record.parentId) {
351
+ const pIdx = this.treeItems.findIndex(t => t.kind === "regular" && t.record.id === item.record.parentId);
352
+ if (pIdx >= 0) {
353
+ this.treeWidget.select(pIdx);
354
+ this.screen.render();
355
+ }
356
+ }
357
+ });
358
+ this.treeWidget.key("escape", () => this.ctx.goBack());
359
+ this.treeWidget.key("r", async () => {
360
+ const item = this.selectedItem();
361
+ if (!item || item.kind !== "regular" || item.record.type !== "Stage")
362
+ return;
363
+ if (item.record.state !== "completed") {
364
+ this.ctx.setStatus(`Stage "${item.record.name}" is not completed`, 3000);
365
+ return;
366
+ }
367
+ const stageRef = item.record.identifier ?? item.record.name;
368
+ const base = `https://dev.azure.com/${(0, api_js_1.enc)(this.ctx.org)}/${(0, api_js_1.enc)(this.ctx.project)}/_apis/build/builds/${this.buildId}`;
369
+ this.ctx.setStatus(`Restarting "${item.record.name}"…`, 0);
370
+ try {
371
+ const token = await this.ctx.getToken();
372
+ await (0, api_js_1.httpPatch)(`${base}/stages/${encodeURIComponent(stageRef)}?${api_js_1.API_VER}`, token, { forceRetryAllJobs: true, state: 1, retryDependencies: true });
373
+ this.ctx.setStatus(`Stage "${item.record.name}" restarted`, 3000);
374
+ if (this.buildId)
375
+ this.poll(this.buildId, this.pollGen);
376
+ }
377
+ catch (e) {
378
+ const raw = e.message;
379
+ const match = raw.match(/^HTTP \d+: ([\s\S]+)/);
380
+ let msg = raw;
381
+ if (match) {
382
+ try {
383
+ msg = JSON.parse(match[1]).message ?? match[1];
384
+ }
385
+ catch {
386
+ msg = match[1];
387
+ }
388
+ }
389
+ this.ctx.setStatus(`Restart failed: ${msg}`, 8000);
390
+ }
391
+ });
392
+ this.logWidget.key(["escape", "backspace", "left"], () => {
393
+ this.treeWidget.focus();
394
+ this.screen.render();
395
+ });
396
+ this.logWidget.key(["f", "end"], () => {
397
+ this.followLog = true;
398
+ this.logWidget.setScrollPerc(100);
399
+ this.screen.render();
400
+ });
401
+ this.logWidget.on("scroll", () => {
402
+ const lb = this.logWidget;
403
+ if (lb.getScrollPerc() < 98)
404
+ this.followLog = false;
405
+ });
406
+ }
407
+ }
408
+ exports.PipelineRunScreen = PipelineRunScreen;
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PipelineRunsScreen = void 0;
37
+ const child_process_1 = require("child_process");
38
+ const blessed = __importStar(require("blessed"));
39
+ const api_js_1 = require("../lib/api.js");
40
+ const format_js_1 = require("../lib/format.js");
41
+ const cache_js_1 = require("../cache.js");
42
+ class PipelineRunsScreen {
43
+ screen;
44
+ ctx;
45
+ widget;
46
+ pipeline = null;
47
+ runs = [];
48
+ get footerText() {
49
+ return (" {cyan-fg}↑↓{/} Navigate {cyan-fg}Enter{/} Open run {cyan-fg}b{/} Browser " +
50
+ "{cyan-fg}r{/} Refresh {cyan-fg}Esc{/} Back {cyan-fg}q{/} Quit");
51
+ }
52
+ constructor(screen, ctx) {
53
+ this.screen = screen;
54
+ this.ctx = ctx;
55
+ this.widget = blessed.list({
56
+ parent: screen, top: 1, left: 0, width: "100%", height: "100%-3",
57
+ border: { type: "line" }, label: " Pipeline Runs ",
58
+ tags: true, keys: true, vi: true, scrollable: true,
59
+ scrollbar: { ch: "│", style: { fg: "blue" } },
60
+ style: {
61
+ border: { fg: "cyan" }, selected: { bg: "blue", fg: "white" },
62
+ focus: { border: { fg: "white" } },
63
+ },
64
+ items: [], hidden: true,
65
+ });
66
+ this.registerKeys();
67
+ }
68
+ show(pipeline) {
69
+ this.pipeline = pipeline;
70
+ this.runs = [];
71
+ this.widget.show();
72
+ this.widget.setLabel(` Runs: ${pipeline.name} `);
73
+ this.widget.setItems([]);
74
+ this.widget.focus();
75
+ this.screen.render();
76
+ this.loadData();
77
+ }
78
+ hide() { this.widget.hide(); }
79
+ getPipeline() { return this.pipeline; }
80
+ refresh() {
81
+ if (!this.pipeline)
82
+ return;
83
+ this.widget.setItems(this.runs.map(format_js_1.formatRunItem));
84
+ this.widget.setLabel(` Runs: ${this.pipeline.name} (${this.runs.length}) `);
85
+ this.screen.render();
86
+ }
87
+ selected() {
88
+ const idx = this.widget.selected ?? 0;
89
+ return this.runs[idx];
90
+ }
91
+ async loadData() {
92
+ if (!this.pipeline)
93
+ return;
94
+ this.ctx.setStatus("Loading runs…", 0);
95
+ try {
96
+ const token = await this.ctx.getToken();
97
+ this.runs = await (0, api_js_1.fetchPipelineRuns)(this.ctx.org, this.ctx.project, this.pipeline.id, token);
98
+ this.refresh();
99
+ this.ctx.setStatus("", 0);
100
+ }
101
+ catch (e) {
102
+ this.ctx.setStatus(`Error: ${e.message.slice(0, 80)}`, 10_000);
103
+ }
104
+ }
105
+ registerKeys() {
106
+ this.widget.key("enter", () => {
107
+ const run = this.selected();
108
+ if (!run)
109
+ return;
110
+ this.ctx.navigate({ view: "pipelineRun", buildId: String(run.id) });
111
+ });
112
+ this.widget.key("b", () => {
113
+ const run = this.selected();
114
+ if (!run)
115
+ return;
116
+ const { org, project } = this.ctx;
117
+ const url = `https://dev.azure.com/${org}/${project}/_build/results?buildId=${run.id}`;
118
+ try {
119
+ (0, child_process_1.spawn)("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
120
+ }
121
+ catch { }
122
+ });
123
+ this.widget.key("r", () => {
124
+ if (!this.pipeline)
125
+ return;
126
+ const { org, project } = this.ctx;
127
+ (0, cache_js_1.clearByPrefix)(`runs_${org}_${project}_${this.pipeline.id}`);
128
+ this.runs = [];
129
+ this.ctx.setStatus("Refreshing…", 0);
130
+ this.loadData();
131
+ });
132
+ this.widget.key("escape", () => this.ctx.goBack());
133
+ }
134
+ }
135
+ exports.PipelineRunsScreen = PipelineRunsScreen;