vg-coder-cli 2.0.15 → 2.0.17

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.
@@ -1,16 +1,17 @@
1
- import { getGitDiff } from '../api.js';
2
- import { showToast, showLoading } from '../utils.js';
1
+ import { getGitStatus, getGitDiff, stageFile, unstageFile, commitChanges, discardChange } from '../api.js';
2
+ import { showToast } from '../utils.js';
3
3
 
4
4
  export function initGitView() {
5
- console.log('[GitView] Initializing...');
6
5
  const toggleBtn = document.getElementById('git-view-toggle');
7
6
  const refreshBtn = document.getElementById('git-refresh-btn');
8
7
 
9
8
  if (toggleBtn) toggleBtn.addEventListener('click', toggleGitMode);
10
- if (refreshBtn) refreshBtn.addEventListener('click', loadGitChanges);
9
+ if (refreshBtn) refreshBtn.addEventListener('click', loadGitData);
11
10
  }
12
11
 
13
12
  let isGitMode = false;
13
+ let currentStaged = [];
14
+ let currentChanges = [];
14
15
 
15
16
  async function toggleGitMode() {
16
17
  const gitContainer = document.getElementById('git-view-container');
@@ -19,20 +20,13 @@ async function toggleGitMode() {
19
20
  const toggleText = document.getElementById('git-toggle-text');
20
21
 
21
22
  isGitMode = !isGitMode;
22
- console.log('[GitView] Toggle Mode:', isGitMode);
23
23
 
24
24
  if (isGitMode) {
25
25
  gitContainer.classList.add('active');
26
26
  toggleBtn.classList.add('active');
27
27
  if (toggleText) toggleText.textContent = 'Close Changes';
28
28
  if (refreshBtn) refreshBtn.style.display = 'flex';
29
-
30
- // Debug kích thước container
31
- const rect = gitContainer.getBoundingClientRect();
32
- console.log('[GitView] Container Size:', rect.width, 'x', rect.height);
33
-
34
- // FIX: Always load changes when opening, removing the empty check
35
- await loadGitChanges();
29
+ await loadGitData();
36
30
  } else {
37
31
  gitContainer.classList.remove('active');
38
32
  toggleBtn.classList.remove('active');
@@ -41,77 +35,337 @@ async function toggleGitMode() {
41
35
  }
42
36
  }
43
37
 
44
- async function loadGitChanges() {
45
- const gitContainer = document.getElementById('git-view-container');
46
- const refreshBtn = document.getElementById('git-refresh-btn');
38
+ async function loadGitData() {
39
+ const container = document.getElementById('git-view-container');
47
40
 
48
- console.log('[GitView] Loading changes...');
41
+ // Initial Structure if empty
42
+ if (!container.querySelector('.git-sidebar')) {
43
+ container.innerHTML = `
44
+ <div class="git-sidebar">
45
+ <div class="git-commit-section">
46
+ <textarea id="git-commit-message" class="git-commit-input" placeholder="Message (⌘Enter to commit)" rows="1"></textarea>
47
+ <button id="git-commit-btn" class="git-commit-btn">
48
+ <span>✓</span> Commit
49
+ </button>
50
+ </div>
51
+ <div class="git-section">
52
+ <div class="git-section-header" id="header-staged">
53
+ <div style="display:flex;align-items:center;gap:5px;">
54
+ <span>STAGED CHANGES</span>
55
+ <span class="git-badge" id="badge-staged">0</span>
56
+ </div>
57
+ <span class="git-btn-action" id="unstage-all" title="Unstage All" style="display:none;">-</span>
58
+ </div>
59
+ <ul class="git-tree-root" id="tree-staged"></ul>
60
+ </div>
61
+ <div class="git-section">
62
+ <div class="git-section-header" id="header-changes">
63
+ <div style="display:flex;align-items:center;gap:5px;">
64
+ <span>CHANGES</span>
65
+ <span class="git-badge" id="badge-changes">0</span>
66
+ </div>
67
+ <div style="display:flex;gap:5px;">
68
+ <span class="git-btn-action destructive" id="discard-all" title="Discard All Changes">↺</span>
69
+ <span class="git-btn-action" id="stage-all" title="Stage All">+</span>
70
+ </div>
71
+ </div>
72
+ <ul class="git-tree-root" id="tree-changes"></ul>
73
+ </div>
74
+ </div>
75
+ <div class="git-diff-area" id="git-diff-viewer">
76
+ <div class="git-empty-state">Select a file to view changes</div>
77
+ </div>
78
+ `;
79
+
80
+ // Bind Actions
81
+ document.getElementById('stage-all').addEventListener('click', async (e) => {
82
+ e.stopPropagation();
83
+ await handleStage('*');
84
+ });
85
+ document.getElementById('unstage-all').addEventListener('click', async (e) => {
86
+ e.stopPropagation();
87
+ await handleUnstage('*');
88
+ });
89
+ document.getElementById('discard-all').addEventListener('click', async (e) => {
90
+ e.stopPropagation();
91
+ await handleDiscard('*');
92
+ });
93
+
94
+ // Commit Actions
95
+ const commitBtn = document.getElementById('git-commit-btn');
96
+ const commitInput = document.getElementById('git-commit-message');
97
+
98
+ commitBtn.addEventListener('click', handleCommit);
99
+ commitInput.addEventListener('keydown', (e) => {
100
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
101
+ e.preventDefault();
102
+ handleCommit();
103
+ }
104
+ });
105
+ }
106
+
107
+ const refreshBtn = document.getElementById('git-refresh-btn');
108
+ if(refreshBtn) refreshBtn.disabled = true;
109
+
110
+ try {
111
+ const { staged, changes } = await getGitStatus();
112
+ currentStaged = staged;
113
+ currentChanges = changes;
114
+ renderTrees();
115
+ } catch (err) {
116
+ showToast('Error loading git status: ' + err.message, 'error');
117
+ } finally {
118
+ if(refreshBtn) refreshBtn.disabled = false;
119
+ }
120
+ }
121
+
122
+ async function handleCommit() {
123
+ const input = document.getElementById('git-commit-message');
124
+ const message = input.value.trim();
125
+ const btn = document.getElementById('git-commit-btn');
126
+
127
+ if (!message) {
128
+ showToast('Please enter a commit message', 'error');
129
+ input.focus();
130
+ return;
131
+ }
132
+
133
+ if (currentStaged.length === 0) {
134
+ showToast('Nothing to commit. Stage changes first.', 'error');
135
+ return;
136
+ }
137
+
138
+ btn.disabled = true;
139
+ btn.innerHTML = '<span>...</span> Committing...';
140
+
141
+ try {
142
+ await commitChanges(message);
143
+ showToast('Commit successful', 'success');
144
+ input.value = '';
145
+ await loadGitData();
146
+ } catch (err) {
147
+ showToast('Commit failed: ' + err.message, 'error');
148
+ } finally {
149
+ btn.disabled = false;
150
+ btn.innerHTML = '<span>✓</span> Commit';
151
+ }
152
+ }
153
+
154
+ function renderTrees() {
155
+ // 1. Render Staged
156
+ const treeStaged = document.getElementById('tree-staged');
157
+ document.getElementById('badge-staged').textContent = currentStaged.length;
158
+ document.getElementById('unstage-all').style.display = currentStaged.length > 0 ? 'block' : 'none';
159
+ treeStaged.innerHTML = '';
49
160
 
50
- if (refreshBtn) {
51
- refreshBtn.style.opacity = '0.5';
52
- refreshBtn.disabled = true;
161
+ if (currentStaged.length > 0) {
162
+ const rootStaged = buildFileTree(currentStaged);
163
+ renderTreeNodes(rootStaged, treeStaged, 'staged', 0);
53
164
  }
165
+
166
+ // 2. Render Changes
167
+ const treeChanges = document.getElementById('tree-changes');
168
+ document.getElementById('badge-changes').textContent = currentChanges.length;
169
+ document.getElementById('discard-all').style.display = currentChanges.length > 0 ? 'block' : 'none';
170
+ treeChanges.innerHTML = '';
54
171
 
55
- // Only show loading if we are replacing content or it's empty
56
- gitContainer.innerHTML = '<div class="git-loading-msg">Loading git changes... (Check Console if stuck)</div>';
172
+ if (currentChanges.length > 0) {
173
+ const rootChanges = buildFileTree(currentChanges);
174
+ renderTreeNodes(rootChanges, treeChanges, 'changes', 0);
175
+ }
176
+ }
57
177
 
58
- try {
59
- // Kiểm tra thư viện
60
- if (typeof Diff2HtmlUI === 'undefined') {
61
- throw new Error('Thư viện Diff2HtmlUI chưa được load!');
62
- }
63
- if (typeof hljs === 'undefined') {
64
- console.warn('⚠️ Highlight.js chưa được load!');
65
- }
178
+ // --- TREE LOGIC ---
179
+
180
+ function buildFileTree(files) {
181
+ const root = {};
182
+ files.forEach(file => {
183
+ const parts = file.path.split('/');
184
+ let current = root;
185
+ parts.forEach((part, index) => {
186
+ if (!current[part]) {
187
+ current[part] = { name: part, children: {}, fileData: null };
188
+ }
189
+ if (index === parts.length - 1) {
190
+ current[part].fileData = file;
191
+ }
192
+ current = current[part].children;
193
+ });
194
+ });
195
+ return root;
196
+ }
197
+
198
+ function renderTreeNodes(nodes, container, type, depth) {
199
+ const items = Object.values(nodes);
200
+ items.sort((a, b) => {
201
+ const aIsFolder = Object.keys(a.children).length > 0;
202
+ const bIsFolder = Object.keys(b.children).length > 0;
203
+ if (aIsFolder && !bIsFolder) return -1;
204
+ if (!aIsFolder && bIsFolder) return 1;
205
+ return a.name.localeCompare(b.name);
206
+ });
207
+
208
+ items.forEach(node => {
209
+ const isFolder = Object.keys(node.children).length > 0;
210
+ const li = document.createElement('li');
211
+ li.className = 'git-tree-node';
66
212
 
67
- const diffString = await getGitDiff();
68
- console.log('[GitView] Diff received, length:', diffString?.length);
213
+ // --- CONTENT ROW ---
214
+ const content = document.createElement('div');
215
+ content.className = 'git-tree-content';
216
+ content.style.paddingLeft = (depth * 16 + 8) + 'px';
69
217
 
70
- if (!diffString || !diffString.trim()) {
71
- gitContainer.innerHTML = `
72
- <div class="git-loading-msg">
73
- <div style="font-size:30px;">✨</div>
74
- <p>No changes detected</p>
75
- </div>`;
218
+ // Arrow
219
+ const arrow = document.createElement('span');
220
+ arrow.className = 'git-arrow';
221
+ arrow.textContent = isFolder ? '▼' : '';
222
+ content.appendChild(arrow);
223
+
224
+ // Icon
225
+ const iconSpan = document.createElement('span');
226
+ iconSpan.className = 'git-icon';
227
+ if (isFolder) {
228
+ iconSpan.textContent = '📂';
76
229
  } else {
77
- console.log('[GitView] Rendering diff...');
78
-
79
- // Cấu hình Diff2Html
80
- const configuration = {
81
- drawFileList: true,
82
- fileListToggle: false,
83
- fileListStartVisible: true,
84
- fileContentToggle: false,
85
- matching: 'lines',
86
- outputFormat: 'side-by-side',
87
- synchronisedScroll: true,
88
- highlight: true,
89
- renderNothingWhenEmpty: true,
90
- colorScheme: 'dark' // Ép buộc Dark Mode từ thư viện
91
- };
92
-
93
- gitContainer.innerHTML = ''; // Clear loading
94
-
95
- const diff2htmlUi = new Diff2HtmlUI(gitContainer, diffString, configuration);
96
- diff2htmlUi.draw();
97
- diff2htmlUi.highlightCode();
230
+ const status = node.fileData.status;
231
+ iconSpan.textContent = getStatusIcon(status);
232
+ iconSpan.className += ' git-status-' + status;
233
+ }
234
+ content.appendChild(iconSpan);
235
+
236
+ // Label
237
+ const label = document.createElement('span');
238
+ label.className = isFolder ? 'git-label git-dir-label' : 'git-label git-file-label';
239
+ label.textContent = node.name;
240
+ content.appendChild(label);
241
+
242
+ // Actions
243
+ if (!isFolder && node.fileData) {
244
+ const actions = document.createElement('div');
245
+ actions.className = 'git-actions';
98
246
 
99
- console.log('[GitView] Render complete.');
247
+ // Discard Button (Only for Changes)
248
+ if (type === 'changes') {
249
+ const discardBtn = document.createElement('button');
250
+ discardBtn.className = 'git-btn-action destructive';
251
+ discardBtn.textContent = '↺';
252
+ discardBtn.title = 'Discard Changes';
253
+ discardBtn.onclick = (e) => { e.stopPropagation(); handleDiscard(node.fileData.path); };
254
+ actions.appendChild(discardBtn);
255
+ }
256
+
257
+ // Stage/Unstage Button
258
+ const btn = document.createElement('button');
259
+ btn.className = 'git-btn-action';
260
+ if (type === 'staged') {
261
+ btn.textContent = '-';
262
+ btn.title = 'Unstage';
263
+ btn.onclick = (e) => { e.stopPropagation(); handleUnstage(node.fileData.path); };
264
+ } else {
265
+ btn.textContent = '+';
266
+ btn.title = 'Stage';
267
+ btn.onclick = (e) => { e.stopPropagation(); handleStage(node.fileData.path); };
268
+ }
269
+ actions.appendChild(btn);
270
+ content.appendChild(actions);
271
+ }
272
+
273
+ // Click Handler
274
+ content.addEventListener('click', (e) => {
275
+ e.stopPropagation();
276
+ if (isFolder) {
277
+ li.classList.toggle('collapsed');
278
+ } else {
279
+ document.querySelectorAll('.git-tree-content').forEach(el => el.classList.remove('selected'));
280
+ content.classList.add('selected');
281
+ loadDiffView(node.fileData.path, type);
282
+ }
283
+ });
284
+
285
+ li.appendChild(content);
286
+
287
+ // --- CHILDREN ---
288
+ if (isFolder) {
289
+ const ul = document.createElement('ul');
290
+ renderTreeNodes(node.children, ul, type, depth + 1);
291
+ li.appendChild(ul);
100
292
  }
101
- showToast('Git changes loaded', 'success');
102
293
 
294
+ container.appendChild(li);
295
+ });
296
+ }
297
+
298
+ function getStatusIcon(status) {
299
+ if(status === 'M') return 'M';
300
+ if(status === 'A' || status === 'U') return 'U';
301
+ if(status === 'D') return 'D';
302
+ if(status === 'R') return 'R';
303
+ return '?';
304
+ }
305
+
306
+ // --- ACTIONS ---
307
+
308
+ async function handleStage(path) {
309
+ try {
310
+ await stageFile(path);
311
+ await loadGitData();
312
+ } catch (err) {
313
+ showToast('Stage failed: ' + err.message, 'error');
314
+ }
315
+ }
316
+
317
+ async function handleUnstage(path) {
318
+ try {
319
+ await unstageFile(path);
320
+ await loadGitData();
103
321
  } catch (err) {
104
- console.error('[GitView] Error:', err);
105
- gitContainer.innerHTML = `
106
- <div class="git-loading-msg" style="color:var(--ios-red);">
107
- <div>⚠️ Error</div>
108
- <div style="font-size:12px;">${err.message}</div>
109
- <div style="font-size:10px; margin-top:5px;">Check F12 Console for details</div>
110
- </div>`;
322
+ showToast('Unstage failed: ' + err.message, 'error');
323
+ }
324
+ }
325
+
326
+ async function handleDiscard(path) {
327
+ const isAll = path === '*';
328
+ const msg = isAll
329
+ ? 'Are you sure you want to discard ALL changes? This is irreversible!'
330
+ : `Discard changes to ${path}? This is irreversible!`;
331
+
332
+ if (confirm(msg)) {
333
+ try {
334
+ await discardChange(path);
335
+ await loadGitData();
336
+ showToast('Changes discarded', 'success');
337
+ } catch (err) {
338
+ showToast('Discard failed: ' + err.message, 'error');
339
+ }
111
340
  }
341
+ }
342
+
343
+ async function loadDiffView(filePath, type) {
344
+ const viewer = document.getElementById('git-diff-viewer');
345
+ viewer.innerHTML = '<div class="git-empty-state">Loading diff...</div>';
112
346
 
113
- if (refreshBtn) {
114
- refreshBtn.style.opacity = '1';
115
- refreshBtn.disabled = false;
347
+ try {
348
+ const diff = await getGitDiff(filePath, type === 'staged' ? 'staged' : 'working');
349
+
350
+ if (!diff || !diff.trim()) {
351
+ viewer.innerHTML = '<div class="git-empty-state">No text changes detected (binary file?)</div>';
352
+ return;
353
+ }
354
+
355
+ viewer.innerHTML = '';
356
+ const ui = new Diff2HtmlUI(viewer, diff, {
357
+ drawFileList: false,
358
+ matching: 'lines',
359
+ outputFormat: 'side-by-side',
360
+ synchronisedScroll: true,
361
+ highlight: true,
362
+ renderNothingWhenEmpty: false,
363
+ colorScheme: 'dark'
364
+ });
365
+ ui.draw();
366
+ ui.highlightCode();
367
+
368
+ } catch (err) {
369
+ viewer.innerHTML = `<div class="git-empty-state" style="color:#f85149">Error loading diff: ${err.message}</div>`;
116
370
  }
117
371
  }
@@ -1,8 +1,3 @@
1
- /**
2
- * Quản lý tính năng Iframe AI Provider
3
- */
4
-
5
- // Danh sách các AI Providers
6
1
  const AI_PROVIDERS = [
7
2
  { id: 'chatgpt', name: 'ChatGPT', url: 'https://chat.openai.com' },
8
3
  { id: 'kimi', name: 'Kimi AI', url: 'https://www.kimi.com' },
@@ -16,6 +11,12 @@ export function initIframeManager() {
16
11
  const iframe = document.getElementById('ai-iframe');
17
12
  const externalLink = document.getElementById('ai-external-link');
18
13
  const placeholderLink = document.getElementById('ai-placeholder-link');
14
+
15
+ // Guide elements
16
+ const guideContainer = document.getElementById('iframe-placeholder');
17
+ const guideToggleBtn = document.getElementById('guide-toggle-btn');
18
+ const guideCloseBtn = document.getElementById('guide-close-btn');
19
+ const guideDoneBtn = document.getElementById('guide-done-btn');
19
20
 
20
21
  if (!select || !iframe) return;
21
22
 
@@ -27,18 +28,20 @@ export function initIframeManager() {
27
28
  select.appendChild(option);
28
29
  });
29
30
 
30
- // 2. Load saved selection or default
31
+ // 2. Load saved selection
31
32
  const savedProviderId = localStorage.getItem('ai_provider') || 'chatgpt';
32
-
33
- // Validate if saved provider still exists
34
33
  const isValid = AI_PROVIDERS.some(p => p.id === savedProviderId);
35
34
  select.value = isValid ? savedProviderId : AI_PROVIDERS[0].id;
36
35
 
37
- // 3. Function to update UI
38
36
  const updateProvider = (providerId) => {
39
37
  const provider = AI_PROVIDERS.find(p => p.id === providerId) || AI_PROVIDERS[0];
40
38
 
41
- iframe.src = provider.url;
39
+ // Reset iframe source to trigger reload
40
+ iframe.src = 'about:blank';
41
+ setTimeout(() => {
42
+ iframe.src = provider.url;
43
+ }, 50);
44
+
42
45
  externalLink.href = provider.url;
43
46
  placeholderLink.href = provider.url;
44
47
  placeholderLink.textContent = `Mở ${provider.name} tab mới ↗`;
@@ -46,11 +49,44 @@ export function initIframeManager() {
46
49
  localStorage.setItem('ai_provider', providerId);
47
50
  };
48
51
 
49
- // 4. Initial update
52
+ // Initial load
50
53
  updateProvider(select.value);
51
54
 
52
- // 5. Event listener
55
+ // Event listeners
53
56
  select.addEventListener('change', (e) => {
54
57
  updateProvider(e.target.value);
55
58
  });
59
+
60
+ // --- GUIDE TOGGLE LOGIC ---
61
+
62
+ const showGuide = () => {
63
+ guideContainer.classList.remove('hidden');
64
+ };
65
+
66
+ const hideGuide = () => {
67
+ guideContainer.classList.add('hidden');
68
+ };
69
+
70
+ const reloadIframe = () => {
71
+ const currentSrc = iframe.src;
72
+ iframe.src = 'about:blank';
73
+ setTimeout(() => {
74
+ iframe.src = currentSrc;
75
+ }, 100);
76
+ };
77
+
78
+ if (guideToggleBtn) {
79
+ guideToggleBtn.addEventListener('click', showGuide);
80
+ }
81
+
82
+ if (guideCloseBtn) {
83
+ guideCloseBtn.addEventListener('click', hideGuide);
84
+ }
85
+
86
+ if (guideDoneBtn) {
87
+ guideDoneBtn.addEventListener('click', () => {
88
+ hideGuide();
89
+ reloadIframe();
90
+ });
91
+ }
56
92
  }
@@ -28,9 +28,6 @@ document.addEventListener('DOMContentLoaded', async () => {
28
28
 
29
29
  // Initialize Terminal System
30
30
  initTerminal();
31
-
32
- // Auto open one terminal on start (Optional)
33
- // setTimeout(() => createNewTerminal(), 500);
34
31
  });
35
32
 
36
33
  async function checkServerStatus() {
@@ -75,40 +72,56 @@ async function loadExtensionPath() {
75
72
  try {
76
73
  const res = await fetch('/api/extension-path');
77
74
  const data = await res.json();
78
- const input = document.getElementById('extension-path-input');
79
- if (data.exists) input.value = data.path;
80
- else {
81
- input.value = "Error: Extension folder not found.";
82
- input.style.color = "var(--ios-red)";
75
+ // Updated ID for the input in center guide
76
+ const input = document.getElementById('extension-path-input-center');
77
+ if (input) {
78
+ if (data.exists) input.value = data.path;
79
+ else {
80
+ input.value = "Error: Extension folder not found.";
81
+ input.style.color = "var(--ios-red)";
82
+ }
83
83
  }
84
84
  } catch (err) {}
85
85
  }
86
86
 
87
- window.toggleExtensionGuide = function() {
88
- const content = document.getElementById('extension-content');
89
- const icon = document.getElementById('ext-toggle-icon');
90
- content.classList.toggle('open');
91
- icon.classList.toggle('open');
92
- }
93
-
94
87
  window.copyExtensionPath = function(event) {
95
- const input = document.getElementById('extension-path-input');
88
+ const input = document.getElementById('extension-path-input-center');
96
89
  const btn = event.currentTarget;
97
- const icon = document.getElementById('ext-copy-icon');
98
- const text = document.getElementById('ext-copy-text');
90
+ const originalText = btn.textContent;
91
+
99
92
  navigator.clipboard.writeText(input.value).then(() => {
100
- showCopiedState(btn, icon, text, '📋', 'Copy Path');
93
+ btn.textContent = '';
94
+ btn.style.background = 'var(--ios-green)';
95
+ btn.style.color = 'white';
96
+ btn.style.borderColor = 'var(--ios-green)';
101
97
  showToast('Đã copy đường dẫn extension', 'success');
98
+
99
+ setTimeout(() => {
100
+ btn.textContent = originalText;
101
+ btn.style.background = '';
102
+ btn.style.color = '';
103
+ btn.style.borderColor = '';
104
+ }, 2000);
102
105
  });
103
106
  }
104
107
 
105
108
  window.copyChromeUrl = function(event) {
106
- const input = document.getElementById('chrome-url-input');
109
+ const input = document.getElementById('chrome-url-input-center');
107
110
  const btn = event.currentTarget;
108
- const originalText = btn.innerHTML;
111
+ const originalText = btn.textContent;
112
+
109
113
  navigator.clipboard.writeText(input.value).then(() => {
110
- btn.innerHTML = '✓';
114
+ btn.textContent = '✓';
115
+ btn.style.background = 'var(--ios-green)';
116
+ btn.style.color = 'white';
117
+ btn.style.borderColor = 'var(--ios-green)';
111
118
  showToast('Đã copy URL', 'success');
112
- setTimeout(() => btn.innerHTML = originalText, 1500);
119
+
120
+ setTimeout(() => {
121
+ btn.textContent = originalText;
122
+ btn.style.background = '';
123
+ btn.style.color = '';
124
+ btn.style.borderColor = '';
125
+ }, 2000);
113
126
  });
114
127
  }
Binary file
package/change.sh DELETED
File without changes
Binary file
Binary file
Binary file