torrent-tui 0.0.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/LICENSE +21 -0
- package/README.md +156 -0
- package/bin/torrent-tui +26 -0
- package/package.json +58 -0
- package/src/app.ts +157 -0
- package/src/config/index.ts +41 -0
- package/src/config/settings.ts +15 -0
- package/src/constants/index.ts +16 -0
- package/src/controllers/app-controller.ts +180 -0
- package/src/index.ts +320 -0
- package/src/layout/add-torrent-dialog.ts +170 -0
- package/src/layout/confirm-dialog.ts +141 -0
- package/src/layout/content-window.ts +80 -0
- package/src/layout/sidebar.ts +121 -0
- package/src/layout/status-bar.ts +79 -0
- package/src/layout/toast-manager.ts +109 -0
- package/src/layout/toast.ts +257 -0
- package/src/layout/torrent-view.ts +250 -0
- package/src/store/index.ts +51 -0
- package/src/theme/default.ts +22 -0
- package/src/theme/index.ts +19 -0
- package/src/theme/types.ts +26 -0
- package/src/torrent/bridge.ts +301 -0
- package/src/torrent/downloader.ts +415 -0
- package/src/torrent/get_peers.ts +212 -0
- package/src/torrent/metadata.ts +190 -0
- package/src/torrent/parser.ts +216 -0
- package/src/torrent/peer/connection.ts +278 -0
- package/src/torrent/peer/handshake.ts +48 -0
- package/src/torrent/peer/listener.ts +52 -0
- package/src/torrent/peer/manager.ts +233 -0
- package/src/torrent/peer/message-buffer.ts +31 -0
- package/src/torrent/peer/peer-id.ts +21 -0
- package/src/torrent/peer/protocol.ts +123 -0
- package/src/torrent/piece-picker.ts +58 -0
- package/src/torrent/session.ts +56 -0
- package/src/torrent/storage.ts +197 -0
- package/src/torrent/tracker/announce.ts +36 -0
- package/src/torrent/tracker/http-tracker.ts +143 -0
- package/src/torrent/tracker/udp-tracker.ts +136 -0
- package/src/torrent/types.ts +25 -0
- package/src/types/layout.ts +6 -0
- package/src/utils/env.ts +8 -0
- package/src/utils/filter.ts +12 -0
- package/src/utils/layout.ts +32 -0
- package/src/utils/paths.ts +17 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { BoxRenderable, type CliRenderer, TextRenderable } from "@opentui/core";
|
|
2
|
+
import { APP_NAME, SIDEBAR_ITEMS } from "../constants";
|
|
3
|
+
import type { AppState, Store } from "../store";
|
|
4
|
+
import { getTheme } from "../theme";
|
|
5
|
+
import type { LayoutDimensions } from "../types/layout";
|
|
6
|
+
|
|
7
|
+
interface SidebarItem {
|
|
8
|
+
text: TextRenderable;
|
|
9
|
+
globalIndex: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class Sidebar {
|
|
13
|
+
private renderer: CliRenderer;
|
|
14
|
+
private store: Store;
|
|
15
|
+
private container: BoxRenderable;
|
|
16
|
+
private titleText: TextRenderable | null = null;
|
|
17
|
+
private itemTexts: SidebarItem[] = [];
|
|
18
|
+
private layout: LayoutDimensions;
|
|
19
|
+
|
|
20
|
+
constructor(renderer: CliRenderer, store: Store, layout: LayoutDimensions) {
|
|
21
|
+
this.renderer = renderer;
|
|
22
|
+
this.store = store;
|
|
23
|
+
this.layout = layout;
|
|
24
|
+
this.container = this.build();
|
|
25
|
+
this.renderer.root.add(this.container);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
update(state?: AppState, focusArea: "sidebar" | "table" = "sidebar"): void {
|
|
29
|
+
const s = state ?? this.store.getState();
|
|
30
|
+
const theme = getTheme();
|
|
31
|
+
const sidebarActive = focusArea === "sidebar";
|
|
32
|
+
|
|
33
|
+
(this.container as unknown as { borderColor: string }).borderColor =
|
|
34
|
+
sidebarActive ? theme.accent : theme.border;
|
|
35
|
+
|
|
36
|
+
for (const item of this.itemTexts) {
|
|
37
|
+
const itemName = SIDEBAR_ITEMS.status[item.globalIndex] ?? "";
|
|
38
|
+
const isSelected = item.globalIndex === s.selectedIndex;
|
|
39
|
+
(item.text as unknown as { content: string }).content =
|
|
40
|
+
`${isSelected && sidebarActive ? "> " : " "}${itemName}`;
|
|
41
|
+
(item.text as unknown as { fg: string }).fg = isSelected
|
|
42
|
+
? (sidebarActive ? theme.accent : theme.fgSecondary)
|
|
43
|
+
: theme.fgPrimary;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
updateLayout(layout: LayoutDimensions): void {
|
|
48
|
+
this.layout = layout;
|
|
49
|
+
this.container.width = layout.sidebar.width;
|
|
50
|
+
this.container.height = layout.sidebar.height;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private build(): BoxRenderable {
|
|
54
|
+
const theme = getTheme();
|
|
55
|
+
const state = this.store.getState();
|
|
56
|
+
|
|
57
|
+
this.container = new BoxRenderable(this.renderer, {
|
|
58
|
+
position: "absolute",
|
|
59
|
+
left: this.layout.sidebar.x,
|
|
60
|
+
top: this.layout.sidebar.y,
|
|
61
|
+
width: this.layout.sidebar.width,
|
|
62
|
+
height: this.layout.sidebar.height,
|
|
63
|
+
border: true,
|
|
64
|
+
borderColor: theme.border,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const titleBox = new BoxRenderable(this.renderer, {
|
|
68
|
+
position: "absolute",
|
|
69
|
+
left: 0,
|
|
70
|
+
top: 0,
|
|
71
|
+
width: this.layout.sidebar.width,
|
|
72
|
+
paddingY: 1,
|
|
73
|
+
paddingX: 1,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
this.titleText = new TextRenderable(this.renderer, {
|
|
77
|
+
content: APP_NAME,
|
|
78
|
+
fg: theme.accent,
|
|
79
|
+
});
|
|
80
|
+
titleBox.add(this.titleText);
|
|
81
|
+
this.container.add(titleBox);
|
|
82
|
+
|
|
83
|
+
let yOffset = 3;
|
|
84
|
+
|
|
85
|
+
const statusTitle = new BoxRenderable(this.renderer, {
|
|
86
|
+
position: "absolute",
|
|
87
|
+
left: 1,
|
|
88
|
+
top: yOffset,
|
|
89
|
+
});
|
|
90
|
+
const statusTitleText = new TextRenderable(this.renderer, {
|
|
91
|
+
content: "Status",
|
|
92
|
+
fg: theme.fgMuted,
|
|
93
|
+
});
|
|
94
|
+
statusTitle.add(statusTitleText);
|
|
95
|
+
this.container.add(statusTitle);
|
|
96
|
+
yOffset += 2;
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < SIDEBAR_ITEMS.status.length; i++) {
|
|
99
|
+
const isSelected = i === state.selectedIndex;
|
|
100
|
+
const itemName = SIDEBAR_ITEMS.status[i];
|
|
101
|
+
|
|
102
|
+
const itemBox = new BoxRenderable(this.renderer, {
|
|
103
|
+
position: "absolute",
|
|
104
|
+
left: 1,
|
|
105
|
+
top: yOffset,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const text = new TextRenderable(this.renderer, {
|
|
109
|
+
content: `${isSelected ? "> " : " "}${itemName}`,
|
|
110
|
+
fg: isSelected ? theme.accent : theme.fgPrimary,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
itemBox.add(text);
|
|
114
|
+
this.container.add(itemBox);
|
|
115
|
+
this.itemTexts.push({ text, globalIndex: i });
|
|
116
|
+
yOffset += 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return this.container;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { BoxRenderable, type CliRenderer, TextRenderable } from "@opentui/core";
|
|
2
|
+
import type { AppState } from "../store";
|
|
3
|
+
import { getTheme } from "../theme";
|
|
4
|
+
import type { LayoutDimensions } from "../types/layout";
|
|
5
|
+
|
|
6
|
+
function formatSpeed(bps: number): string {
|
|
7
|
+
if (bps >= 1_000_000) return `${(bps / 1_000_000).toFixed(1)} MB/s`;
|
|
8
|
+
if (bps >= 1_000) return `${Math.round(bps / 1_000)} KB/s`;
|
|
9
|
+
return `${bps} B/s`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class StatusBar {
|
|
13
|
+
private renderer: CliRenderer;
|
|
14
|
+
private layout: LayoutDimensions;
|
|
15
|
+
private container: BoxRenderable;
|
|
16
|
+
private leftText: TextRenderable;
|
|
17
|
+
private rightText: TextRenderable;
|
|
18
|
+
|
|
19
|
+
constructor(renderer: CliRenderer, layout: LayoutDimensions) {
|
|
20
|
+
this.renderer = renderer;
|
|
21
|
+
this.layout = layout;
|
|
22
|
+
const { container, leftText, rightText } = this.build();
|
|
23
|
+
this.container = container;
|
|
24
|
+
this.leftText = leftText;
|
|
25
|
+
this.rightText = rightText;
|
|
26
|
+
this.renderer.root.add(this.container);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
update(state: AppState): void {
|
|
30
|
+
const theme = getTheme();
|
|
31
|
+
const dl = formatSpeed(state.totalDownloadBps);
|
|
32
|
+
const ul = formatSpeed(state.totalUploadBps);
|
|
33
|
+
const count = state.torrents.length;
|
|
34
|
+
const status = count === 0 ? "idle" : `${count} torrent${count !== 1 ? "s" : ""}`;
|
|
35
|
+
|
|
36
|
+
(this.leftText as unknown as { content: string }).content =
|
|
37
|
+
` ↓ ${dl} ↑ ${ul} | ${status}`;
|
|
38
|
+
(this.leftText as unknown as { fg: string }).fg = theme.fgPrimary;
|
|
39
|
+
|
|
40
|
+
(this.rightText as unknown as { content: string }).content =
|
|
41
|
+
"Tab focus Space pause d del D del+files a add q quit ";
|
|
42
|
+
(this.rightText as unknown as { fg: string }).fg = theme.fgMuted;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
updateLayout(layout: LayoutDimensions): void {
|
|
46
|
+
this.layout = layout;
|
|
47
|
+
(this.container as unknown as { top: number }).top = layout.statusBar.y;
|
|
48
|
+
(this.container as unknown as { width: number }).width = layout.statusBar.width;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private build(): { container: BoxRenderable; leftText: TextRenderable; rightText: TextRenderable } {
|
|
52
|
+
const theme = getTheme();
|
|
53
|
+
|
|
54
|
+
const container = new BoxRenderable(this.renderer, {
|
|
55
|
+
position: "absolute",
|
|
56
|
+
left: this.layout.statusBar.x,
|
|
57
|
+
top: this.layout.statusBar.y,
|
|
58
|
+
width: this.layout.statusBar.width,
|
|
59
|
+
height: this.layout.statusBar.height,
|
|
60
|
+
flexDirection: "row",
|
|
61
|
+
justifyContent: "space-between",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const leftText = new TextRenderable(this.renderer, {
|
|
65
|
+
content: " ↓ 0 B/s ↑ 0 B/s | idle",
|
|
66
|
+
fg: theme.fgPrimary,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const rightText = new TextRenderable(this.renderer, {
|
|
70
|
+
content: "j/k nav a add q quit ",
|
|
71
|
+
fg: theme.fgMuted,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
container.add(leftText);
|
|
75
|
+
container.add(rightText);
|
|
76
|
+
|
|
77
|
+
return { container, leftText, rightText };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { CliRenderer } from "@opentui/core";
|
|
2
|
+
import { TOAST_MARGIN, TOAST_MAX_COUNT, TOAST_WIDTH } from "../constants";
|
|
3
|
+
import type { LayoutDimensions } from "../types/layout";
|
|
4
|
+
import { Toast, type ToastConfig } from "./toast";
|
|
5
|
+
|
|
6
|
+
export class ToastManager {
|
|
7
|
+
private renderer: CliRenderer;
|
|
8
|
+
private layout: LayoutDimensions;
|
|
9
|
+
private toasts: Toast[] = [];
|
|
10
|
+
private dismissInterval: ReturnType<typeof setInterval>;
|
|
11
|
+
|
|
12
|
+
constructor(renderer: CliRenderer, layout: LayoutDimensions) {
|
|
13
|
+
this.renderer = renderer;
|
|
14
|
+
this.layout = layout;
|
|
15
|
+
|
|
16
|
+
this.dismissInterval = setInterval(() => {
|
|
17
|
+
this.tick();
|
|
18
|
+
}, 100);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
show(config: ToastConfig): Toast {
|
|
22
|
+
const x = this.layout.terminal.width - TOAST_WIDTH - TOAST_MARGIN;
|
|
23
|
+
const y = this.calculateYPosition();
|
|
24
|
+
|
|
25
|
+
const toast = new Toast(this.renderer, config, this.layout, x, y);
|
|
26
|
+
this.toasts.push(toast);
|
|
27
|
+
toast.addToRenderer();
|
|
28
|
+
this.repositionToasts();
|
|
29
|
+
|
|
30
|
+
if (this.toasts.length > TOAST_MAX_COUNT) {
|
|
31
|
+
const oldest = this.toasts.shift();
|
|
32
|
+
if (oldest) {
|
|
33
|
+
oldest.dismiss();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return toast;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private calculateYPosition(): number {
|
|
41
|
+
let y = TOAST_MARGIN;
|
|
42
|
+
for (const toast of this.toasts) {
|
|
43
|
+
y += toast.getHeight() + TOAST_MARGIN;
|
|
44
|
+
}
|
|
45
|
+
return y;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private repositionToasts(): void {
|
|
49
|
+
let y = TOAST_MARGIN;
|
|
50
|
+
for (const toast of this.toasts) {
|
|
51
|
+
toast.setY(y);
|
|
52
|
+
y += toast.getHeight() + TOAST_MARGIN;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
dismiss(id: string): void {
|
|
57
|
+
const index = this.toasts.findIndex((t) => t.getId() === id);
|
|
58
|
+
if (index !== -1) {
|
|
59
|
+
const toast = this.toasts[index];
|
|
60
|
+
if (toast) {
|
|
61
|
+
toast.dismiss();
|
|
62
|
+
this.toasts.splice(index, 1);
|
|
63
|
+
this.repositionToasts();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
updateLayout(layout: LayoutDimensions): void {
|
|
69
|
+
this.layout = layout;
|
|
70
|
+
for (const toast of this.toasts) {
|
|
71
|
+
toast.updateLayout(layout);
|
|
72
|
+
}
|
|
73
|
+
this.repositionToasts();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
handleInput(key: string): boolean {
|
|
77
|
+
const topToast = this.toasts[this.toasts.length - 1];
|
|
78
|
+
if (topToast) {
|
|
79
|
+
return topToast.handleInput(key);
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
tick(): void {
|
|
85
|
+
const toRemove: Toast[] = [];
|
|
86
|
+
|
|
87
|
+
for (const toast of this.toasts) {
|
|
88
|
+
if (toast.shouldAutoDismiss()) {
|
|
89
|
+
toRemove.push(toast);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const toast of toRemove) {
|
|
94
|
+
this.dismiss(toast.getId());
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
destroy(): void {
|
|
99
|
+
clearInterval(this.dismissInterval);
|
|
100
|
+
for (const toast of this.toasts) {
|
|
101
|
+
toast.dismiss();
|
|
102
|
+
}
|
|
103
|
+
this.toasts = [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getToasts(): Toast[] {
|
|
107
|
+
return this.toasts;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { BoxRenderable, type CliRenderer, TextRenderable } from "@opentui/core";
|
|
2
|
+
import {
|
|
3
|
+
TOAST_DEFAULT_DURATION,
|
|
4
|
+
TOAST_MARGIN,
|
|
5
|
+
TOAST_WIDTH,
|
|
6
|
+
} from "../constants";
|
|
7
|
+
import { getTheme } from "../theme";
|
|
8
|
+
import type { LayoutDimensions } from "../types/layout";
|
|
9
|
+
|
|
10
|
+
export type ToastType = "info" | "success" | "warning" | "error" | "action";
|
|
11
|
+
|
|
12
|
+
export interface ToastConfig {
|
|
13
|
+
id: string;
|
|
14
|
+
type: ToastType;
|
|
15
|
+
title: string;
|
|
16
|
+
message: string;
|
|
17
|
+
duration?: number | null;
|
|
18
|
+
dismissable?: boolean;
|
|
19
|
+
onDismiss?: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class Toast {
|
|
23
|
+
private renderer: CliRenderer;
|
|
24
|
+
private config: ToastConfig;
|
|
25
|
+
private x: number;
|
|
26
|
+
private y: number;
|
|
27
|
+
private height: number;
|
|
28
|
+
|
|
29
|
+
private createdAt: number;
|
|
30
|
+
private dismissed: boolean = false;
|
|
31
|
+
private addedToRenderer: boolean = false;
|
|
32
|
+
|
|
33
|
+
private layout: LayoutDimensions;
|
|
34
|
+
private container: BoxRenderable;
|
|
35
|
+
private titleLabel: TextRenderable;
|
|
36
|
+
private messageLabel: TextRenderable;
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
renderer: CliRenderer,
|
|
40
|
+
config: ToastConfig,
|
|
41
|
+
layout: LayoutDimensions,
|
|
42
|
+
x: number,
|
|
43
|
+
y: number,
|
|
44
|
+
) {
|
|
45
|
+
this.renderer = renderer;
|
|
46
|
+
this.config = {
|
|
47
|
+
dismissable: true,
|
|
48
|
+
duration: TOAST_DEFAULT_DURATION,
|
|
49
|
+
...config,
|
|
50
|
+
};
|
|
51
|
+
this.layout = layout;
|
|
52
|
+
this.x = x;
|
|
53
|
+
this.y = y;
|
|
54
|
+
this.createdAt = Date.now();
|
|
55
|
+
|
|
56
|
+
this.height = this.calculateHeight();
|
|
57
|
+
this.container = this.createContainer();
|
|
58
|
+
this.titleLabel = this.createTitleLabel();
|
|
59
|
+
this.messageLabel = this.createMessageLabel();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private calculateHeight(): number {
|
|
63
|
+
let height = 3;
|
|
64
|
+
const maxLineWidth = TOAST_WIDTH - 4;
|
|
65
|
+
const messageLines = this.wrapText(this.config.message, maxLineWidth);
|
|
66
|
+
height += messageLines.length;
|
|
67
|
+
return height;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private wrapText(text: string, maxWidth: number): string[] {
|
|
71
|
+
const words = text.split(" ");
|
|
72
|
+
const lines: string[] = [];
|
|
73
|
+
let currentLine = "";
|
|
74
|
+
|
|
75
|
+
for (const word of words) {
|
|
76
|
+
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
|
77
|
+
if (testLine.length <= maxWidth) {
|
|
78
|
+
currentLine = testLine;
|
|
79
|
+
} else {
|
|
80
|
+
if (currentLine) lines.push(currentLine);
|
|
81
|
+
currentLine = word;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (currentLine) lines.push(currentLine);
|
|
85
|
+
|
|
86
|
+
return lines;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private getBorderColor(): string {
|
|
90
|
+
const theme = getTheme();
|
|
91
|
+
switch (this.config.type) {
|
|
92
|
+
case "info":
|
|
93
|
+
case "action":
|
|
94
|
+
return theme.accent;
|
|
95
|
+
case "success":
|
|
96
|
+
return theme.success;
|
|
97
|
+
case "warning":
|
|
98
|
+
return theme.warning;
|
|
99
|
+
case "error":
|
|
100
|
+
return theme.error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private getIcon(): string {
|
|
105
|
+
switch (this.config.type) {
|
|
106
|
+
case "info":
|
|
107
|
+
return "i";
|
|
108
|
+
case "success":
|
|
109
|
+
return "+";
|
|
110
|
+
case "warning":
|
|
111
|
+
return "!";
|
|
112
|
+
case "error":
|
|
113
|
+
return "x";
|
|
114
|
+
case "action":
|
|
115
|
+
return ">";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private createContainer(): BoxRenderable {
|
|
120
|
+
const theme = getTheme();
|
|
121
|
+
return new BoxRenderable(this.renderer, {
|
|
122
|
+
position: "absolute",
|
|
123
|
+
left: this.x,
|
|
124
|
+
top: this.y,
|
|
125
|
+
width: TOAST_WIDTH,
|
|
126
|
+
height: this.height,
|
|
127
|
+
borderColor: this.getBorderColor(),
|
|
128
|
+
borderStyle: "single",
|
|
129
|
+
backgroundColor: theme.bgPrimary,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private createTitleLabel(): TextRenderable {
|
|
134
|
+
const _theme = getTheme();
|
|
135
|
+
const icon = this.getIcon();
|
|
136
|
+
return new TextRenderable(this.renderer, {
|
|
137
|
+
content: `${icon} ${this.config.title}`,
|
|
138
|
+
fg: this.getBorderColor(),
|
|
139
|
+
position: "absolute",
|
|
140
|
+
left: this.x + 2,
|
|
141
|
+
top: this.y + 1,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private createMessageLabel(): TextRenderable {
|
|
146
|
+
const theme = getTheme();
|
|
147
|
+
const messageLines = this.wrapText(this.config.message, TOAST_WIDTH - 4);
|
|
148
|
+
const messageContent = messageLines.join("\n");
|
|
149
|
+
return new TextRenderable(this.renderer, {
|
|
150
|
+
content: messageContent,
|
|
151
|
+
fg: theme.fgPrimary,
|
|
152
|
+
position: "absolute",
|
|
153
|
+
left: this.x + 2,
|
|
154
|
+
top: this.y + 2,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
shouldAutoDismiss(): boolean {
|
|
159
|
+
if (this.config.duration === null || this.config.duration === undefined) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return Date.now() - this.createdAt >= this.config.duration;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
addToRenderer(): void {
|
|
166
|
+
if (this.addedToRenderer || this.dismissed) return;
|
|
167
|
+
|
|
168
|
+
this.renderer.root.add(this.container);
|
|
169
|
+
this.renderer.root.add(this.titleLabel);
|
|
170
|
+
this.renderer.root.add(this.messageLabel);
|
|
171
|
+
this.addedToRenderer = true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
removeFromRenderer(): void {
|
|
175
|
+
if (!this.addedToRenderer) return;
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
this.container.destroy();
|
|
179
|
+
this.titleLabel.destroy();
|
|
180
|
+
this.messageLabel.destroy();
|
|
181
|
+
} catch {
|
|
182
|
+
// Elements might not exist
|
|
183
|
+
}
|
|
184
|
+
this.addedToRenderer = false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
updatePosition(x: number, y: number): void {
|
|
188
|
+
this.x = x;
|
|
189
|
+
this.y = y;
|
|
190
|
+
|
|
191
|
+
(this.container as unknown as { left: number }).left = x;
|
|
192
|
+
(this.container as unknown as { top: number }).top = y;
|
|
193
|
+
(this.titleLabel as unknown as { left: number }).left = x + 2;
|
|
194
|
+
(this.titleLabel as unknown as { top: number }).top = y + 1;
|
|
195
|
+
(this.messageLabel as unknown as { left: number }).left = x + 2;
|
|
196
|
+
(this.messageLabel as unknown as { top: number }).top = y + 2;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
handleInput(key: string): boolean {
|
|
200
|
+
if (this.config.dismissable && key === "escape") {
|
|
201
|
+
this.dismiss();
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
dismiss(): void {
|
|
208
|
+
if (this.dismissed) return;
|
|
209
|
+
|
|
210
|
+
this.dismissed = true;
|
|
211
|
+
this.removeFromRenderer();
|
|
212
|
+
this.config.onDismiss?.();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
getId(): string {
|
|
216
|
+
return this.config.id;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
getHeight(): number {
|
|
220
|
+
return this.height;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
isDismissed(): boolean {
|
|
224
|
+
return this.dismissed;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
getDuration(): number | null | undefined {
|
|
228
|
+
return this.config.duration;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
setY(y: number): void {
|
|
232
|
+
this.y = y;
|
|
233
|
+
this.updatePosition(this.x, y);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getY(): number {
|
|
237
|
+
return this.y;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
isAddedToRenderer(): boolean {
|
|
241
|
+
return this.addedToRenderer;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
updateLayout(layout: LayoutDimensions): void {
|
|
245
|
+
this.layout = layout;
|
|
246
|
+
|
|
247
|
+
const newX = layout.terminal.width - TOAST_WIDTH - TOAST_MARGIN;
|
|
248
|
+
if (newX !== this.x) {
|
|
249
|
+
this.x = newX;
|
|
250
|
+
this.updatePosition(this.x, this.y);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
getWidth(): number {
|
|
255
|
+
return TOAST_WIDTH;
|
|
256
|
+
}
|
|
257
|
+
}
|