jupyterlab_vscode_icons_extension 1.0.112 → 1.1.6

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/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
2
+ import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
2
3
  import { LabIcon } from '@jupyterlab/ui-components';
3
4
  import { getIconSVG } from './icons';
4
5
  const PLUGIN_ID = 'jupyterlab_vscode_icons_extension:plugin';
@@ -126,16 +127,8 @@ const fileTypeConfigs = [
126
127
  iconName: 'file-type-perl',
127
128
  group: 'enableLanguageIcons'
128
129
  },
129
- {
130
- extensions: ['.sh', '.bash', '.zsh'],
131
- iconName: 'file-type-shell',
132
- group: 'enableLanguageIcons'
133
- },
134
- {
135
- extensions: ['.bat', '.cmd'],
136
- iconName: 'file-type-shell',
137
- group: 'enableLanguageIcons'
138
- },
130
+ // Shell scripts (.sh, .bash, .zsh) and batch files (.bat, .cmd) use custom icons with black backgrounds
131
+ // Registered separately below with custom SVGs
139
132
  {
140
133
  extensions: ['.ps1'],
141
134
  iconName: 'file-type-powershell',
@@ -366,8 +359,8 @@ const plugin = {
366
359
  id: PLUGIN_ID,
367
360
  description: 'Jupyterlab extension with a shameless rip-off of the vscode-icons into our beloved environment',
368
361
  autoStart: true,
369
- optional: [ISettingRegistry],
370
- activate: (app, settingRegistry) => {
362
+ optional: [ISettingRegistry, IDefaultFileBrowser],
363
+ activate: (app, settingRegistry, defaultFileBrowser) => {
371
364
  const { docRegistry } = app;
372
365
  // Function to inject CSS that overrides Jupytext icons
373
366
  const injectIconOverrideCSS = () => {
@@ -430,10 +423,24 @@ const plugin = {
430
423
  const claudeDataUri = `data:image/svg+xml;base64,${btoa(claudeSvg)}`;
431
424
  const readmeDataUri = `data:image/svg+xml;base64,${btoa(readmeSvg)}`;
432
425
  const pdfDataUri = `data:image/svg+xml;base64,${btoa(pdfSvg)}`;
433
- const wordDataUri = wordSvg ? `data:image/svg+xml;base64,${btoa(wordSvg)}` : '';
434
- const excelDataUri = excelSvg ? `data:image/svg+xml;base64,${btoa(excelSvg)}` : '';
435
- const powerpointDataUri = powerpointSvg ? `data:image/svg+xml;base64,${btoa(powerpointSvg)}` : '';
436
- const svgFileDataUri = svgFileSvg ? `data:image/svg+xml;base64,${btoa(svgFileSvg)}` : '';
426
+ const wordDataUri = wordSvg
427
+ ? `data:image/svg+xml;base64,${btoa(wordSvg)}`
428
+ : '';
429
+ const excelDataUri = excelSvg
430
+ ? `data:image/svg+xml;base64,${btoa(excelSvg)}`
431
+ : '';
432
+ const powerpointDataUri = powerpointSvg
433
+ ? `data:image/svg+xml;base64,${btoa(powerpointSvg)}`
434
+ : '';
435
+ const svgFileDataUri = svgFileSvg
436
+ ? `data:image/svg+xml;base64,${btoa(svgFileSvg)}`
437
+ : '';
438
+ const uvSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 330 330"><rect height="100%" width="100%" rx="66" fill="#26102f"/><path fill="#d256dc" d="M 65,65 h92 v130 h16 v-130 h92 v200 h-16 v-20 h-8 a20,20 0 0 1 -20,20 h-136 a20,20 0 0 1 -20,-20 z"/></svg>';
439
+ const uvDataUri = `data:image/svg+xml;base64,${btoa(uvSvg)}`;
440
+ const pytestSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 474 542"><path fill="#696969" d="M21,43h431c12,0 21,9 21,21c0,12-9,21-21,21H21c-12,0-21-9-21-21c0-12,9-21,21-21z"/><path fill="#009fe3" d="M25,0h87v20H25z"/><path fill="#c7d302" d="M138,0h87v20h-87z"/><path fill="#f07e16" d="M250,0h87v20h-87z"/><path fill="#df2815" d="M362,0h87v20h-87z"/><path fill="#df2815" d="M362,107h87v147h-87z"/><path fill="#f07e16" d="M250,107h87v238h-87z"/><path fill="#c7d302" d="M138,107h87v357h-87z"/><path fill="#009fe3" d="M25,107h87v435h-87z"/></svg>';
441
+ const pytestDataUri = `data:image/svg+xml;base64,${btoa(pytestSvg)}`;
442
+ const pythonPackageSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><linearGradient id="ppa" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0" stop-color="#387eb8"/><stop offset="1" stop-color="#366994"/></linearGradient><linearGradient id="ppb" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0" stop-color="#ffe052"/><stop offset="1" stop-color="#ffc331"/></linearGradient></defs><g transform="scale(-1,1) translate(-32,0)"><path d="M27.4,5.5H18.1L16,9.7H4.3V26.5H29.5V5.5Zm0,4.2H19.2l1.1-2.1h7.1Z" fill="#58af7b"/><path d="M20.9,11c-5.1,0-4.8,2.2-4.8,2.2v2.3H21v.7H14.2S11,15.8,11,21s2.9,5,2.9,5h1.7V23.6a2.7,2.7,0,0,1,2.8-2.9h4.8a2.6,2.6,0,0,0,2.7-2.6V13.7S26.2,11,20.9,11Zm-2.7,1.5a.9.9,0,1,1-.8.9.9.9,0,0,1,.8-.9Z" fill="url(#ppa)"/><path d="M21.1,31c5.1,0,4.8-2.2,4.8-2.2V26.5H21v-.7h6.8S31,26.1,31,21s-2.9-5-2.9-5h-1.7v2.4a2.7,2.7,0,0,1-2.8,2.9H18.8a2.6,2.6,0,0,0-2.7,2.6v4.4S15.7,31,21,31Zm2.7-1.5a.9.9,0,1,1,.8-.9.9.9,0,0,1-.8.9Z" fill="url(#ppb)"/></g></svg>';
443
+ const pythonPackageDataUri = `data:image/svg+xml;base64,${btoa(pythonPackageSvg)}`;
437
444
  // Inject CSS that overrides icons for .py and .md files
438
445
  // Note: Jupytext marks .py and .md files as type="notebook", so we need to
439
446
  // use JavaScript to detect and mark these files for CSS targeting
@@ -589,6 +596,54 @@ const plugin = {
589
596
  background-repeat: no-repeat;
590
597
  background-position: center;
591
598
  }
599
+
600
+ /* Override uv.lock file icon with UV icon */
601
+ .jp-DirListing-item[data-uv-lock] .jp-DirListing-itemIcon svg,
602
+ .jp-DirListing-item[data-uv-lock] .jp-DirListing-itemIcon img {
603
+ display: none !important;
604
+ }
605
+ .jp-DirListing-item[data-uv-lock] .jp-DirListing-itemIcon::before {
606
+ content: '';
607
+ display: inline-block;
608
+ width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
609
+ height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
610
+ background-image: url('${uvDataUri}');
611
+ background-size: contain;
612
+ background-repeat: no-repeat;
613
+ background-position: center;
614
+ }
615
+
616
+ /* Override pytest-related file icons */
617
+ .jp-DirListing-item[data-pytest] .jp-DirListing-itemIcon svg,
618
+ .jp-DirListing-item[data-pytest] .jp-DirListing-itemIcon img {
619
+ display: none !important;
620
+ }
621
+ .jp-DirListing-item[data-pytest] .jp-DirListing-itemIcon::before {
622
+ content: '';
623
+ display: inline-block;
624
+ width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
625
+ height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
626
+ background-image: url('${pytestDataUri}');
627
+ background-size: contain;
628
+ background-repeat: no-repeat;
629
+ background-position: center;
630
+ }
631
+
632
+ /* Override Python package folder icons */
633
+ .jp-DirListing-item[data-python-package] .jp-DirListing-itemIcon svg,
634
+ .jp-DirListing-item[data-python-package] .jp-DirListing-itemIcon img {
635
+ display: none !important;
636
+ }
637
+ .jp-DirListing-item[data-python-package] .jp-DirListing-itemIcon::before {
638
+ content: '';
639
+ display: inline-block;
640
+ width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
641
+ height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
642
+ background-image: url('${pythonPackageDataUri}');
643
+ background-size: contain;
644
+ background-repeat: no-repeat;
645
+ background-position: center;
646
+ }
592
647
  `;
593
648
  // Add CSS to make JavaScript and .env icons less bright
594
649
  style.textContent += `
@@ -604,21 +659,75 @@ const plugin = {
604
659
  filter: brightness(0.85) saturate(0.75);
605
660
  }
606
661
 
607
- /* Color shell script icons - JupyterLab orange for Linux shells (.sh, .bash, .zsh) */
608
- .jp-DirListing-item[data-file-type="vscode-file-type-shell"][data-shell-type="linux"] .jp-DirListing-itemIcon svg {
609
- filter: brightness(0) saturate(100%) invert(58%) sepia(76%) saturate(3113%) hue-rotate(1deg) brightness(101%) contrast(101%);
610
- }
611
-
612
- /* Color shell script icons - pale blue for Windows shells (.bat, .cmd) */
613
- .jp-DirListing-item[data-file-type="vscode-file-type-shell"][data-shell-type="windows"] .jp-DirListing-itemIcon svg {
614
- filter: hue-rotate(180deg) saturate(0.6) brightness(1.2);
615
- }
616
662
 
617
663
  /* Make hidden items darker (items starting with .) */
618
664
  .jp-DirListing-item[data-is-dot] {
619
665
  opacity: 55% !important;
620
666
  }
621
667
  `;
668
+ // Cache for Python package folder checks
669
+ const pythonPackageCache = new Map();
670
+ // Folders to exclude from Python package checking
671
+ const excludedFolderPatterns = [
672
+ /^\.git$/,
673
+ /^\.hg$/,
674
+ /^\.svn$/,
675
+ /^\.venv$/,
676
+ /^venv$/,
677
+ /^\.env$/,
678
+ /^env$/,
679
+ /^node_modules$/,
680
+ /^__pycache__$/,
681
+ /^\.ipynb_checkpoints$/,
682
+ /^\.pytest_cache$/,
683
+ /^\.mypy_cache$/,
684
+ /^\.ruff_cache$/,
685
+ /^\.tox$/,
686
+ /^\.nox$/,
687
+ /^\.coverage$/,
688
+ /^htmlcov$/,
689
+ /^dist$/,
690
+ /^build$/,
691
+ /^\.eggs$/,
692
+ /\.egg-info$/,
693
+ /\.dist-info$/
694
+ ];
695
+ const isExcludedFolder = (name) => {
696
+ return excludedFolderPatterns.some(pattern => pattern.test(name));
697
+ };
698
+ // Check if a folder is a Python package (contains __init__.py)
699
+ // Use the file browser's model to get the correct path
700
+ const checkPythonPackage = async (folderName) => {
701
+ var _a;
702
+ // Skip excluded folders
703
+ if (isExcludedFolder(folderName)) {
704
+ return false;
705
+ }
706
+ // Get the current path from the file browser model
707
+ if (!defaultFileBrowser) {
708
+ return false;
709
+ }
710
+ const currentPath = defaultFileBrowser.model.path;
711
+ const fullPath = currentPath
712
+ ? `${currentPath}/${folderName}`
713
+ : folderName;
714
+ if (pythonPackageCache.has(fullPath)) {
715
+ return pythonPackageCache.get(fullPath);
716
+ }
717
+ try {
718
+ // Use JupyterLab's contents manager with the correct path from file browser
719
+ const contents = app.serviceManager.contents;
720
+ const model = await contents.get(fullPath, { content: true });
721
+ const hasInit = ((_a = model.content) === null || _a === void 0 ? void 0 : _a.some((item) => item.name === '__init__.py')) ||
722
+ false;
723
+ pythonPackageCache.set(fullPath, hasInit);
724
+ return hasInit;
725
+ }
726
+ catch (_b) {
727
+ pythonPackageCache.set(fullPath, false);
728
+ return false;
729
+ }
730
+ };
622
731
  // Add a MutationObserver to mark special files in the file browser
623
732
  const markSpecialFiles = () => {
624
733
  // Process ALL items - clear wrong attributes and set correct ones
@@ -658,23 +767,6 @@ const plugin = {
658
767
  item.removeAttribute('data-jupytext-py');
659
768
  item.removeAttribute('data-jupytext-md');
660
769
  }
661
- // Handle shell script files - ONLY set attribute if BOTH conditions match
662
- if (fileType === 'vscode-file-type-shell') {
663
- if (name.endsWith('.sh') || name.endsWith('.bash') || name.endsWith('.zsh')) {
664
- item.setAttribute('data-shell-type', 'linux');
665
- }
666
- else if (name.endsWith('.bat') || name.endsWith('.cmd')) {
667
- item.setAttribute('data-shell-type', 'windows');
668
- }
669
- else {
670
- // Shell file type but wrong extension - clear attribute
671
- item.removeAttribute('data-shell-type');
672
- }
673
- }
674
- else {
675
- // Not a shell file - always clear shell-type attribute
676
- item.removeAttribute('data-shell-type');
677
- }
678
770
  // Handle PDF and Office files by extension (override native JupyterLab icons)
679
771
  const nameLower = name.toLowerCase();
680
772
  // Clear all office/pdf attributes first
@@ -686,13 +778,17 @@ const plugin = {
686
778
  if (nameLower.endsWith('.pdf')) {
687
779
  item.setAttribute('data-vscode-pdf', 'true');
688
780
  }
689
- else if (nameLower.endsWith('.doc') || nameLower.endsWith('.docx')) {
781
+ else if (nameLower.endsWith('.doc') ||
782
+ nameLower.endsWith('.docx')) {
690
783
  item.setAttribute('data-vscode-word', 'true');
691
784
  }
692
- else if (nameLower.endsWith('.xls') || nameLower.endsWith('.xlsx') || nameLower.endsWith('.xlsm')) {
785
+ else if (nameLower.endsWith('.xls') ||
786
+ nameLower.endsWith('.xlsx') ||
787
+ nameLower.endsWith('.xlsm')) {
693
788
  item.setAttribute('data-vscode-excel', 'true');
694
789
  }
695
- else if (nameLower.endsWith('.ppt') || nameLower.endsWith('.pptx')) {
790
+ else if (nameLower.endsWith('.ppt') ||
791
+ nameLower.endsWith('.pptx')) {
696
792
  item.setAttribute('data-vscode-powerpoint', 'true');
697
793
  }
698
794
  // Force SVG icon for .svg files (override any incorrect file type detection)
@@ -700,6 +796,36 @@ const plugin = {
700
796
  if (nameLower.endsWith('.svg')) {
701
797
  item.setAttribute('data-vscode-svg-override', 'true');
702
798
  }
799
+ // Force UV icon for uv.lock file
800
+ item.removeAttribute('data-uv-lock');
801
+ if (nameLower === 'uv.lock') {
802
+ item.setAttribute('data-uv-lock', 'true');
803
+ }
804
+ // Force pytest icon for pytest-related files
805
+ item.removeAttribute('data-pytest');
806
+ if (nameLower === '.coverage' ||
807
+ nameLower === 'pytest.ini' ||
808
+ nameLower === 'conftest.py') {
809
+ item.setAttribute('data-pytest', 'true');
810
+ }
811
+ // Check if this is a directory (folder)
812
+ const isDir = fileType === 'directory' ||
813
+ item.classList.contains('jp-DirListing-directory');
814
+ if (isDir) {
815
+ // Check if this folder is a Python package (async)
816
+ // Pass just the folder name - checkPythonPackage gets current path from file browser
817
+ checkPythonPackage(name).then(isPythonPackage => {
818
+ if (isPythonPackage) {
819
+ item.setAttribute('data-python-package', 'true');
820
+ }
821
+ else {
822
+ item.removeAttribute('data-python-package');
823
+ }
824
+ });
825
+ }
826
+ else {
827
+ item.removeAttribute('data-python-package');
828
+ }
703
829
  });
704
830
  };
705
831
  // Watch for changes in the file browser
@@ -874,6 +1000,49 @@ const plugin = {
874
1000
  icon: mcpIcon
875
1001
  });
876
1002
  }
1003
+ // Register shell scripts with custom black background and desaturated orange icon
1004
+ if (settings.enableLanguageIcons) {
1005
+ const shellSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
1006
+ <rect x="1" y="3" width="30" height="26" rx="2" fill="#1a1a1a"/>
1007
+ <path fill="#e8b070" d="M29.4 27.6H2.5V4.5h26.9Zm-25.9-1h24.9V5.5H3.5Z"/>
1008
+ <path fill="#e8b070" d="m6.077 19.316l-.555-.832l4.844-3.229l-4.887-4.071l.641-.768l5.915 4.928zM12.7 18.2h7.8v1h-7.8zM2.5 5.5h26.9v1.9H2.5z"/>
1009
+ </svg>`;
1010
+ const shellIcon = new LabIcon({
1011
+ name: 'shell-icon',
1012
+ svgstr: shellSvg
1013
+ });
1014
+ docRegistry.addFileType({
1015
+ name: 'vscode-shell',
1016
+ displayName: 'Shell Script',
1017
+ extensions: ['.sh', '.bash', '.zsh'],
1018
+ fileFormat: 'text',
1019
+ contentType: 'file',
1020
+ icon: shellIcon
1021
+ });
1022
+ }
1023
+ // Register batch files with custom black background and desaturated blue icon
1024
+ if (settings.enableLanguageIcons) {
1025
+ const batchSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
1026
+ <rect x="1" y="3" width="30" height="26" rx="2" fill="#1a1a1a"/>
1027
+ <path fill="#80c8f0" d="M29.4 27.6H2.5V4.5h26.9Zm-25.9-1h24.9V5.5H3.5Z"/>
1028
+ <path fill="#80c8f0" d="m6.077 19.316l-.555-.832l4.844-3.229l-4.887-4.071l.641-.768l5.915 4.928zM12.7 18.2h7.8v1h-7.8zM2.5 5.5h26.9v1.9H2.5z"/>
1029
+ </svg>`;
1030
+ const batchIcon = new LabIcon({
1031
+ name: 'batch-icon',
1032
+ svgstr: batchSvg
1033
+ });
1034
+ docRegistry.addFileType({
1035
+ name: 'vscode-batch',
1036
+ displayName: 'Batch File',
1037
+ extensions: ['.bat', '.cmd'],
1038
+ fileFormat: 'text',
1039
+ contentType: 'file',
1040
+ icon: batchIcon
1041
+ });
1042
+ }
1043
+ // Note: uv.lock icon is handled via MutationObserver + CSS override
1044
+ // (see injectIconOverrideCSS function) since pattern-only registration
1045
+ // doesn't work reliably for files without standard extensions
877
1046
  };
878
1047
  // Debounce timer for settings change alert
879
1048
  let settingsChangeTimeout = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_vscode_icons_extension",
3
- "version": "1.0.112",
3
+ "version": "1.1.6",
4
4
  "description": "Jupyterlab extension with a shameless rip-off of the vscode-icons into our beloved environment",
5
5
  "keywords": [
6
6
  "jupyter",
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  JupyterFrontEndPlugin
4
4
  } from '@jupyterlab/application';
5
5
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
6
+ import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
6
7
  import { LabIcon } from '@jupyterlab/ui-components';
7
8
  import { getIconSVG } from './icons';
8
9
 
@@ -155,16 +156,8 @@ const fileTypeConfigs: IFileTypeConfig[] = [
155
156
  iconName: 'file-type-perl',
156
157
  group: 'enableLanguageIcons'
157
158
  },
158
- {
159
- extensions: ['.sh', '.bash', '.zsh'],
160
- iconName: 'file-type-shell',
161
- group: 'enableLanguageIcons'
162
- },
163
- {
164
- extensions: ['.bat', '.cmd'],
165
- iconName: 'file-type-shell',
166
- group: 'enableLanguageIcons'
167
- },
159
+ // Shell scripts (.sh, .bash, .zsh) and batch files (.bat, .cmd) use custom icons with black backgrounds
160
+ // Registered separately below with custom SVGs
168
161
  {
169
162
  extensions: ['.ps1'],
170
163
  iconName: 'file-type-powershell',
@@ -373,7 +366,8 @@ const fileTypeConfigs: IFileTypeConfig[] = [
373
366
  group: 'enableConfigIcons'
374
367
  },
375
368
  {
376
- pattern: '^(terraform\\.tfvars\\..*|\\.terraform\\.lock\\..*|\\.terraform\\.tfstate\\.lock\\..*)$',
369
+ pattern:
370
+ '^(terraform\\.tfvars\\..*|\\.terraform\\.lock\\..*|\\.terraform\\.tfstate\\.lock\\..*)$',
377
371
  extensions: [],
378
372
  iconName: 'file-type-terraform',
379
373
  group: 'enableConfigIcons'
@@ -403,16 +397,16 @@ const plugin: JupyterFrontEndPlugin<void> = {
403
397
  description:
404
398
  'Jupyterlab extension with a shameless rip-off of the vscode-icons into our beloved environment',
405
399
  autoStart: true,
406
- optional: [ISettingRegistry],
400
+ optional: [ISettingRegistry, IDefaultFileBrowser],
407
401
  activate: (
408
402
  app: JupyterFrontEnd,
409
- settingRegistry: ISettingRegistry | null
403
+ settingRegistry: ISettingRegistry | null,
404
+ defaultFileBrowser: IDefaultFileBrowser | null
410
405
  ) => {
411
406
  const { docRegistry } = app;
412
407
 
413
408
  // Function to inject CSS that overrides Jupytext icons
414
409
  const injectIconOverrideCSS = () => {
415
-
416
410
  // Get icons: Claude (VSCode), Office (VSCode)
417
411
  const claudeIcon = createLabIcon('file-type-claude');
418
412
  const wordIcon = createLabIcon('file-type-word');
@@ -478,10 +472,27 @@ const plugin: JupyterFrontEndPlugin<void> = {
478
472
  const claudeDataUri = `data:image/svg+xml;base64,${btoa(claudeSvg)}`;
479
473
  const readmeDataUri = `data:image/svg+xml;base64,${btoa(readmeSvg)}`;
480
474
  const pdfDataUri = `data:image/svg+xml;base64,${btoa(pdfSvg)}`;
481
- const wordDataUri = wordSvg ? `data:image/svg+xml;base64,${btoa(wordSvg)}` : '';
482
- const excelDataUri = excelSvg ? `data:image/svg+xml;base64,${btoa(excelSvg)}` : '';
483
- const powerpointDataUri = powerpointSvg ? `data:image/svg+xml;base64,${btoa(powerpointSvg)}` : '';
484
- const svgFileDataUri = svgFileSvg ? `data:image/svg+xml;base64,${btoa(svgFileSvg)}` : '';
475
+ const wordDataUri = wordSvg
476
+ ? `data:image/svg+xml;base64,${btoa(wordSvg)}`
477
+ : '';
478
+ const excelDataUri = excelSvg
479
+ ? `data:image/svg+xml;base64,${btoa(excelSvg)}`
480
+ : '';
481
+ const powerpointDataUri = powerpointSvg
482
+ ? `data:image/svg+xml;base64,${btoa(powerpointSvg)}`
483
+ : '';
484
+ const svgFileDataUri = svgFileSvg
485
+ ? `data:image/svg+xml;base64,${btoa(svgFileSvg)}`
486
+ : '';
487
+ const uvSvg =
488
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 330 330"><rect height="100%" width="100%" rx="66" fill="#26102f"/><path fill="#d256dc" d="M 65,65 h92 v130 h16 v-130 h92 v200 h-16 v-20 h-8 a20,20 0 0 1 -20,20 h-136 a20,20 0 0 1 -20,-20 z"/></svg>';
489
+ const uvDataUri = `data:image/svg+xml;base64,${btoa(uvSvg)}`;
490
+ const pytestSvg =
491
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 474 542"><path fill="#696969" d="M21,43h431c12,0 21,9 21,21c0,12-9,21-21,21H21c-12,0-21-9-21-21c0-12,9-21,21-21z"/><path fill="#009fe3" d="M25,0h87v20H25z"/><path fill="#c7d302" d="M138,0h87v20h-87z"/><path fill="#f07e16" d="M250,0h87v20h-87z"/><path fill="#df2815" d="M362,0h87v20h-87z"/><path fill="#df2815" d="M362,107h87v147h-87z"/><path fill="#f07e16" d="M250,107h87v238h-87z"/><path fill="#c7d302" d="M138,107h87v357h-87z"/><path fill="#009fe3" d="M25,107h87v435h-87z"/></svg>';
492
+ const pytestDataUri = `data:image/svg+xml;base64,${btoa(pytestSvg)}`;
493
+ const pythonPackageSvg =
494
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><linearGradient id="ppa" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0" stop-color="#387eb8"/><stop offset="1" stop-color="#366994"/></linearGradient><linearGradient id="ppb" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0" stop-color="#ffe052"/><stop offset="1" stop-color="#ffc331"/></linearGradient></defs><g transform="scale(-1,1) translate(-32,0)"><path d="M27.4,5.5H18.1L16,9.7H4.3V26.5H29.5V5.5Zm0,4.2H19.2l1.1-2.1h7.1Z" fill="#58af7b"/><path d="M20.9,11c-5.1,0-4.8,2.2-4.8,2.2v2.3H21v.7H14.2S11,15.8,11,21s2.9,5,2.9,5h1.7V23.6a2.7,2.7,0,0,1,2.8-2.9h4.8a2.6,2.6,0,0,0,2.7-2.6V13.7S26.2,11,20.9,11Zm-2.7,1.5a.9.9,0,1,1-.8.9.9.9,0,0,1,.8-.9Z" fill="url(#ppa)"/><path d="M21.1,31c5.1,0,4.8-2.2,4.8-2.2V26.5H21v-.7h6.8S31,26.1,31,21s-2.9-5-2.9-5h-1.7v2.4a2.7,2.7,0,0,1-2.8,2.9H18.8a2.6,2.6,0,0,0-2.7,2.6v4.4S15.7,31,21,31Zm2.7-1.5a.9.9,0,1,1,.8-.9.9.9,0,0,1-.8.9Z" fill="url(#ppb)"/></g></svg>';
495
+ const pythonPackageDataUri = `data:image/svg+xml;base64,${btoa(pythonPackageSvg)}`;
485
496
 
486
497
  // Inject CSS that overrides icons for .py and .md files
487
498
  // Note: Jupytext marks .py and .md files as type="notebook", so we need to
@@ -638,6 +649,54 @@ const plugin: JupyterFrontEndPlugin<void> = {
638
649
  background-repeat: no-repeat;
639
650
  background-position: center;
640
651
  }
652
+
653
+ /* Override uv.lock file icon with UV icon */
654
+ .jp-DirListing-item[data-uv-lock] .jp-DirListing-itemIcon svg,
655
+ .jp-DirListing-item[data-uv-lock] .jp-DirListing-itemIcon img {
656
+ display: none !important;
657
+ }
658
+ .jp-DirListing-item[data-uv-lock] .jp-DirListing-itemIcon::before {
659
+ content: '';
660
+ display: inline-block;
661
+ width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
662
+ height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
663
+ background-image: url('${uvDataUri}');
664
+ background-size: contain;
665
+ background-repeat: no-repeat;
666
+ background-position: center;
667
+ }
668
+
669
+ /* Override pytest-related file icons */
670
+ .jp-DirListing-item[data-pytest] .jp-DirListing-itemIcon svg,
671
+ .jp-DirListing-item[data-pytest] .jp-DirListing-itemIcon img {
672
+ display: none !important;
673
+ }
674
+ .jp-DirListing-item[data-pytest] .jp-DirListing-itemIcon::before {
675
+ content: '';
676
+ display: inline-block;
677
+ width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
678
+ height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
679
+ background-image: url('${pytestDataUri}');
680
+ background-size: contain;
681
+ background-repeat: no-repeat;
682
+ background-position: center;
683
+ }
684
+
685
+ /* Override Python package folder icons */
686
+ .jp-DirListing-item[data-python-package] .jp-DirListing-itemIcon svg,
687
+ .jp-DirListing-item[data-python-package] .jp-DirListing-itemIcon img {
688
+ display: none !important;
689
+ }
690
+ .jp-DirListing-item[data-python-package] .jp-DirListing-itemIcon::before {
691
+ content: '';
692
+ display: inline-block;
693
+ width: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
694
+ height: calc(var(--jp-ui-font-size1, 13px) * var(--jp-custom-icon-scale, 1.5));
695
+ background-image: url('${pythonPackageDataUri}');
696
+ background-size: contain;
697
+ background-repeat: no-repeat;
698
+ background-position: center;
699
+ }
641
700
  `;
642
701
 
643
702
  // Add CSS to make JavaScript and .env icons less bright
@@ -654,15 +713,6 @@ const plugin: JupyterFrontEndPlugin<void> = {
654
713
  filter: brightness(0.85) saturate(0.75);
655
714
  }
656
715
 
657
- /* Color shell script icons - JupyterLab orange for Linux shells (.sh, .bash, .zsh) */
658
- .jp-DirListing-item[data-file-type="vscode-file-type-shell"][data-shell-type="linux"] .jp-DirListing-itemIcon svg {
659
- filter: brightness(0) saturate(100%) invert(58%) sepia(76%) saturate(3113%) hue-rotate(1deg) brightness(101%) contrast(101%);
660
- }
661
-
662
- /* Color shell script icons - pale blue for Windows shells (.bat, .cmd) */
663
- .jp-DirListing-item[data-file-type="vscode-file-type-shell"][data-shell-type="windows"] .jp-DirListing-itemIcon svg {
664
- filter: hue-rotate(180deg) saturate(0.6) brightness(1.2);
665
- }
666
716
 
667
717
  /* Make hidden items darker (items starting with .) */
668
718
  .jp-DirListing-item[data-is-dot] {
@@ -670,6 +720,76 @@ const plugin: JupyterFrontEndPlugin<void> = {
670
720
  }
671
721
  `;
672
722
 
723
+ // Cache for Python package folder checks
724
+ const pythonPackageCache: Map<string, boolean> = new Map();
725
+
726
+ // Folders to exclude from Python package checking
727
+ const excludedFolderPatterns = [
728
+ /^\.git$/,
729
+ /^\.hg$/,
730
+ /^\.svn$/,
731
+ /^\.venv$/,
732
+ /^venv$/,
733
+ /^\.env$/,
734
+ /^env$/,
735
+ /^node_modules$/,
736
+ /^__pycache__$/,
737
+ /^\.ipynb_checkpoints$/,
738
+ /^\.pytest_cache$/,
739
+ /^\.mypy_cache$/,
740
+ /^\.ruff_cache$/,
741
+ /^\.tox$/,
742
+ /^\.nox$/,
743
+ /^\.coverage$/,
744
+ /^htmlcov$/,
745
+ /^dist$/,
746
+ /^build$/,
747
+ /^\.eggs$/,
748
+ /\.egg-info$/,
749
+ /\.dist-info$/
750
+ ];
751
+
752
+ const isExcludedFolder = (name: string): boolean => {
753
+ return excludedFolderPatterns.some(pattern => pattern.test(name));
754
+ };
755
+
756
+ // Check if a folder is a Python package (contains __init__.py)
757
+ // Use the file browser's model to get the correct path
758
+ const checkPythonPackage = async (
759
+ folderName: string
760
+ ): Promise<boolean> => {
761
+ // Skip excluded folders
762
+ if (isExcludedFolder(folderName)) {
763
+ return false;
764
+ }
765
+
766
+ // Get the current path from the file browser model
767
+ if (!defaultFileBrowser) {
768
+ return false;
769
+ }
770
+ const currentPath = defaultFileBrowser.model.path;
771
+ const fullPath = currentPath
772
+ ? `${currentPath}/${folderName}`
773
+ : folderName;
774
+
775
+ if (pythonPackageCache.has(fullPath)) {
776
+ return pythonPackageCache.get(fullPath)!;
777
+ }
778
+ try {
779
+ // Use JupyterLab's contents manager with the correct path from file browser
780
+ const contents = app.serviceManager.contents;
781
+ const model = await contents.get(fullPath, { content: true });
782
+ const hasInit =
783
+ model.content?.some((item: any) => item.name === '__init__.py') ||
784
+ false;
785
+ pythonPackageCache.set(fullPath, hasInit);
786
+ return hasInit;
787
+ } catch {
788
+ pythonPackageCache.set(fullPath, false);
789
+ return false;
790
+ }
791
+ };
792
+
673
793
  // Add a MutationObserver to mark special files in the file browser
674
794
  const markSpecialFiles = () => {
675
795
  // Process ALL items - clear wrong attributes and set correct ones
@@ -712,21 +832,6 @@ const plugin: JupyterFrontEndPlugin<void> = {
712
832
  item.removeAttribute('data-jupytext-md');
713
833
  }
714
834
 
715
- // Handle shell script files - ONLY set attribute if BOTH conditions match
716
- if (fileType === 'vscode-file-type-shell') {
717
- if (name.endsWith('.sh') || name.endsWith('.bash') || name.endsWith('.zsh')) {
718
- item.setAttribute('data-shell-type', 'linux');
719
- } else if (name.endsWith('.bat') || name.endsWith('.cmd')) {
720
- item.setAttribute('data-shell-type', 'windows');
721
- } else {
722
- // Shell file type but wrong extension - clear attribute
723
- item.removeAttribute('data-shell-type');
724
- }
725
- } else {
726
- // Not a shell file - always clear shell-type attribute
727
- item.removeAttribute('data-shell-type');
728
- }
729
-
730
835
  // Handle PDF and Office files by extension (override native JupyterLab icons)
731
836
  const nameLower = name.toLowerCase();
732
837
 
@@ -739,11 +844,21 @@ const plugin: JupyterFrontEndPlugin<void> = {
739
844
  // Set the correct attribute based on extension
740
845
  if (nameLower.endsWith('.pdf')) {
741
846
  item.setAttribute('data-vscode-pdf', 'true');
742
- } else if (nameLower.endsWith('.doc') || nameLower.endsWith('.docx')) {
847
+ } else if (
848
+ nameLower.endsWith('.doc') ||
849
+ nameLower.endsWith('.docx')
850
+ ) {
743
851
  item.setAttribute('data-vscode-word', 'true');
744
- } else if (nameLower.endsWith('.xls') || nameLower.endsWith('.xlsx') || nameLower.endsWith('.xlsm')) {
852
+ } else if (
853
+ nameLower.endsWith('.xls') ||
854
+ nameLower.endsWith('.xlsx') ||
855
+ nameLower.endsWith('.xlsm')
856
+ ) {
745
857
  item.setAttribute('data-vscode-excel', 'true');
746
- } else if (nameLower.endsWith('.ppt') || nameLower.endsWith('.pptx')) {
858
+ } else if (
859
+ nameLower.endsWith('.ppt') ||
860
+ nameLower.endsWith('.pptx')
861
+ ) {
747
862
  item.setAttribute('data-vscode-powerpoint', 'true');
748
863
  }
749
864
 
@@ -752,6 +867,40 @@ const plugin: JupyterFrontEndPlugin<void> = {
752
867
  if (nameLower.endsWith('.svg')) {
753
868
  item.setAttribute('data-vscode-svg-override', 'true');
754
869
  }
870
+
871
+ // Force UV icon for uv.lock file
872
+ item.removeAttribute('data-uv-lock');
873
+ if (nameLower === 'uv.lock') {
874
+ item.setAttribute('data-uv-lock', 'true');
875
+ }
876
+
877
+ // Force pytest icon for pytest-related files
878
+ item.removeAttribute('data-pytest');
879
+ if (
880
+ nameLower === '.coverage' ||
881
+ nameLower === 'pytest.ini' ||
882
+ nameLower === 'conftest.py'
883
+ ) {
884
+ item.setAttribute('data-pytest', 'true');
885
+ }
886
+
887
+ // Check if this is a directory (folder)
888
+ const isDir =
889
+ fileType === 'directory' ||
890
+ item.classList.contains('jp-DirListing-directory');
891
+ if (isDir) {
892
+ // Check if this folder is a Python package (async)
893
+ // Pass just the folder name - checkPythonPackage gets current path from file browser
894
+ checkPythonPackage(name).then(isPythonPackage => {
895
+ if (isPythonPackage) {
896
+ item.setAttribute('data-python-package', 'true');
897
+ } else {
898
+ item.removeAttribute('data-python-package');
899
+ }
900
+ });
901
+ } else {
902
+ item.removeAttribute('data-python-package');
903
+ }
755
904
  });
756
905
  };
757
906
 
@@ -957,6 +1106,56 @@ const plugin: JupyterFrontEndPlugin<void> = {
957
1106
  icon: mcpIcon
958
1107
  });
959
1108
  }
1109
+
1110
+ // Register shell scripts with custom black background and desaturated orange icon
1111
+ if (settings.enableLanguageIcons) {
1112
+ const shellSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
1113
+ <rect x="1" y="3" width="30" height="26" rx="2" fill="#1a1a1a"/>
1114
+ <path fill="#e8b070" d="M29.4 27.6H2.5V4.5h26.9Zm-25.9-1h24.9V5.5H3.5Z"/>
1115
+ <path fill="#e8b070" d="m6.077 19.316l-.555-.832l4.844-3.229l-4.887-4.071l.641-.768l5.915 4.928zM12.7 18.2h7.8v1h-7.8zM2.5 5.5h26.9v1.9H2.5z"/>
1116
+ </svg>`;
1117
+
1118
+ const shellIcon = new LabIcon({
1119
+ name: 'shell-icon',
1120
+ svgstr: shellSvg
1121
+ });
1122
+
1123
+ docRegistry.addFileType({
1124
+ name: 'vscode-shell',
1125
+ displayName: 'Shell Script',
1126
+ extensions: ['.sh', '.bash', '.zsh'],
1127
+ fileFormat: 'text',
1128
+ contentType: 'file',
1129
+ icon: shellIcon
1130
+ });
1131
+ }
1132
+
1133
+ // Register batch files with custom black background and desaturated blue icon
1134
+ if (settings.enableLanguageIcons) {
1135
+ const batchSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
1136
+ <rect x="1" y="3" width="30" height="26" rx="2" fill="#1a1a1a"/>
1137
+ <path fill="#80c8f0" d="M29.4 27.6H2.5V4.5h26.9Zm-25.9-1h24.9V5.5H3.5Z"/>
1138
+ <path fill="#80c8f0" d="m6.077 19.316l-.555-.832l4.844-3.229l-4.887-4.071l.641-.768l5.915 4.928zM12.7 18.2h7.8v1h-7.8zM2.5 5.5h26.9v1.9H2.5z"/>
1139
+ </svg>`;
1140
+
1141
+ const batchIcon = new LabIcon({
1142
+ name: 'batch-icon',
1143
+ svgstr: batchSvg
1144
+ });
1145
+
1146
+ docRegistry.addFileType({
1147
+ name: 'vscode-batch',
1148
+ displayName: 'Batch File',
1149
+ extensions: ['.bat', '.cmd'],
1150
+ fileFormat: 'text',
1151
+ contentType: 'file',
1152
+ icon: batchIcon
1153
+ });
1154
+ }
1155
+
1156
+ // Note: uv.lock icon is handled via MutationObserver + CSS override
1157
+ // (see injectIconOverrideCSS function) since pattern-only registration
1158
+ // doesn't work reliably for files without standard extensions
960
1159
  };
961
1160
 
962
1161
  // Debounce timer for settings change alert
@@ -986,7 +1185,6 @@ const plugin: JupyterFrontEndPlugin<void> = {
986
1185
  }
987
1186
  });
988
1187
 
989
-
990
1188
  // Debounce the alert to show only once when multiple settings change
991
1189
  if (settingsChangeTimeout) {
992
1190
  clearTimeout(settingsChangeTimeout);
package/style/base.css CHANGED
@@ -47,5 +47,5 @@
47
47
 
48
48
  /* Make hidden items darker (items starting with .) */
49
49
  .jp-DirListing-item[data-is-dot] {
50
- opacity: 55% !important;
50
+ opacity: 0.55 !important;
51
51
  }