From eeaeb459f719c7017af7cf4483227953f4129801 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Tue, 26 Dec 2023 09:38:59 -0800 Subject: [PATCH 01/62] python env management UI and env selector popup updates --- .eslintignore | 2 + .prettierignore | 2 + package.json | 5 +- scripts/copyassets.js | 19 +- src/assets/ellipsis-vertical.svg | 1 + src/assets/info-icon.svg | 1 + src/assets/uFuzzy.iife.min.js | 2 + src/main/app.ts | 97 +- src/main/cli.ts | 109 ++- src/main/eventtypes.ts | 12 +- src/main/main.ts | 20 +- src/main/progressview/preload.ts | 2 +- src/main/pythonenvdialog/preload.ts | 160 ++++ src/main/pythonenvdialog/pythonenvdialog.ts | 891 ++++++++++++++++++ src/main/pythonenvselectpopup/preload.ts | 16 + .../pythonenvselectpopup.ts | 163 +++- src/main/sessionwindow/sessionwindow.ts | 8 + src/main/settingsdialog/preload.ts | 11 +- src/main/utils.ts | 39 + src/main/welcomeview/preload.ts | 2 +- webpack.preload.js | 1 + yarn.lock | 66 +- 22 files changed, 1522 insertions(+), 107 deletions(-) create mode 100644 src/assets/ellipsis-vertical.svg create mode 100644 src/assets/info-icon.svg create mode 100644 src/assets/uFuzzy.iife.min.js create mode 100644 src/main/pythonenvdialog/preload.ts create mode 100644 src/main/pythonenvdialog/pythonenvdialog.ts diff --git a/.eslintignore b/.eslintignore index bd505289..c16e7917 100644 --- a/.eslintignore +++ b/.eslintignore @@ -19,3 +19,5 @@ typedoc-theme/ .vscode/ env_installer + +src/assets/uFuzzy.iife.min.js diff --git a/.prettierignore b/.prettierignore index 49c1ac07..28758f2e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,3 +11,5 @@ .vscode/ env_installer + +src/assets/uFuzzy.iife.min.js diff --git a/package.json b/package.json index 47a5e828..f77d4e63 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,8 @@ "prettier": "prettier --write \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", "prettier:check": "prettier --check \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", "stylelint": "yarn stylelint:check --fix", - "stylelint:check": "stylelint \"**/*.css\"" + "stylelint:check": "stylelint \"**/*.css\"", + "update-ufuzzy": "shx cp node_modules/@leeoniya/ufuzzy/dist/uFuzzy.iife.min.js src/assets/" }, "build": { "appId": "org.jupyter.jupyterlab-desktop", @@ -176,6 +177,7 @@ "@types/yargs": "^17.0.18", "@typescript-eslint/eslint-plugin": "~5.28.0", "@typescript-eslint/parser": "~5.28.0", + "@leeoniya/ufuzzy": "1.0.14", "electron": "^27.0.2", "electron-builder": "^24.6.4", "electron-notarize": "^1.2.2", @@ -191,6 +193,7 @@ "prettier": "~2.1.1", "read-package-tree": "^5.1.6", "rimraf": "~3.0.0", + "shx": "^0.3.4", "stylelint": "^15.10.1", "stylelint-config-prettier": "^9.0.3", "stylelint-config-recommended": "^6.0.0", diff --git a/scripts/copyassets.js b/scripts/copyassets.js index 28fc638b..22d5da25 100644 --- a/scripts/copyassets.js +++ b/scripts/copyassets.js @@ -49,22 +49,9 @@ function copyAssests() { path.join(dest, '../app-assets', 'titlebarview', 'titlebar.html') ); - fs.copySync( - path.join(srcDir, 'assets', 'icon.svg'), - path.join(dest, '../app-assets', 'icon.svg') - ); - fs.copySync( - path.join(srcDir, 'assets', 'progress-logo.svg'), - path.join(dest, '../app-assets', 'progress-logo.svg') - ); - fs.copySync( - path.join(srcDir, 'assets', 'jupyterlab-wordmark.svg'), - path.join(dest, '../app-assets', 'jupyterlab-wordmark.svg') - ); - fs.copySync( - path.join(srcDir, 'assets', 'copyable-span.js'), - path.join(dest, '../app-assets', 'copyable-span.js') - ); + fs.copySync(path.join(srcDir, 'assets'), path.join(dest, '../app-assets'), { + recursive: true + }); const toolkitPath = path.join( '../node_modules', diff --git a/src/assets/ellipsis-vertical.svg b/src/assets/ellipsis-vertical.svg new file mode 100644 index 00000000..d5ef4e3b --- /dev/null +++ b/src/assets/ellipsis-vertical.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/info-icon.svg b/src/assets/info-icon.svg new file mode 100644 index 00000000..f5975635 --- /dev/null +++ b/src/assets/info-icon.svg @@ -0,0 +1 @@ + diff --git a/src/assets/uFuzzy.iife.min.js b/src/assets/uFuzzy.iife.min.js new file mode 100644 index 00000000..a0861c4d --- /dev/null +++ b/src/assets/uFuzzy.iife.min.js @@ -0,0 +1,2 @@ +/*! https://github.com/leeoniya/uFuzzy (v1.0.14) */ +var uFuzzy=function(){"use strict";const e=new Intl.Collator("en",{numeric:!0,sensitivity:"base"}).compare,t=1/0,l=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),n="eexxaacctt",r=(e,t,l)=>e.replace("A-Z",t).replace("a-z",l),i={unicode:!1,alpha:null,interSplit:"[^A-Za-z\\d']+",intraSplit:"[a-z][A-Z]",intraBound:"[A-Za-z]\\d|\\d[A-Za-z]|[a-z][A-Z]",interLft:0,interRgt:0,interChars:".",interIns:t,intraChars:"[a-z\\d']",intraIns:null,intraContr:"'[a-z]{1,2}\\b",intraMode:0,intraSlice:[1,t],intraSub:null,intraTrn:null,intraDel:null,intraFilt:()=>!0,sort:(t,l)=>{let{idx:n,chars:r,terms:i,interLft2:s,interLft1:a,start:g,intraIns:f,interIns:h}=t;return n.map(((e,t)=>t)).sort(((t,u)=>r[u]-r[t]||f[t]-f[u]||i[u]+s[u]+.5*a[u]-(i[t]+s[t]+.5*a[t])||h[t]-h[u]||g[t]-g[u]||e(l[n[t]],l[n[u]])))}},s=(e,l)=>0==l?"":1==l?e+"??":l==t?e+"*?":e+`{0,${l}}?`,a="(?:\\b|_)";function g(e){e=Object.assign({},i,e);let{unicode:t,interLft:g,interRgt:f,intraMode:u,intraSlice:c,intraIns:o,intraSub:p,intraTrn:d,intraDel:m,intraContr:x,intraSplit:b,interSplit:R,intraBound:L,intraChars:A}=e;o??=u,p??=u,d??=u,m??=u;let S=e.letters??e.alpha;if(null!=S){let e=S.toLocaleUpperCase(),t=S.toLocaleLowerCase();R=r(R,e,t),b=r(b,e,t),L=r(L,e,t),A=r(A,e,t),x=r(x,e,t)}let E=t?"u":"";const I='".+?"',z=RegExp(I,"gi"+E),C=RegExp(`(?:\\s+|^)-(?:${A}+|${I})`,"gi"+E);let{intraRules:y}=e;null==y&&(y=e=>{let t=i.intraSlice,l=0,n=0,r=0,s=0;if(/[^\d]/.test(e)){let i=e.length;i>4?(t=c,l=o,n=p,r=d,s=m):3>i||(r=Math.min(d,1),4==i&&(l=Math.min(o,1)))}return{intraSlice:t,intraIns:l,intraSub:n,intraTrn:r,intraDel:s}});let k=!!b,j=RegExp(b,"g"+E),$=RegExp(R,"g"+E),w=RegExp("^"+R+"|"+R+"$","g"+E),Z=RegExp(x,"gi"+E);const M=e=>{let t=[];e=(e=e.replace(z,(e=>(t.push(e),n)))).replace(w,"").toLocaleLowerCase(),k&&(e=e.replace(j,(e=>e[0]+" "+e[1])));let l=0;return e.split($).filter((e=>""!=e)).map((e=>e===n?t[l++]:e))},D=/[^\d]+|\d+/g,T=(t,n=0,r=!1)=>{let i=M(t);if(0==i.length)return[];let h,c=Array(i.length).fill("");if(i=i.map(((e,t)=>e.replace(Z,(e=>(c[t]=e,""))))),1==u)h=i.map(((e,t)=>{if('"'===e[0])return l(e.slice(1,-1));let n="";for(let l of e.matchAll(D)){let e=l[0],{intraSlice:r,intraIns:i,intraSub:a,intraTrn:g,intraDel:f}=y(e);if(i+a+g+f==0)n+=e+c[t];else{let[l,h]=r,u=e.slice(0,l),o=e.slice(h),p=e.slice(l,h);1==i&&1==u.length&&u!=p[0]&&(u+="(?!"+u+")");let d=p.length,m=[e];if(a)for(let e=0;d>e;e++)m.push(u+p.slice(0,e)+A+p.slice(e+1)+o);if(g)for(let e=0;d-1>e;e++)p[e]!=p[e+1]&&m.push(u+p.slice(0,e)+p[e+1]+p[e]+p.slice(e+2)+o);if(f)for(let e=0;d>e;e++)m.push(u+p.slice(0,e+1)+"?"+p.slice(e+1)+o);if(i){let e=s(A,1);for(let t=0;d>t;t++)m.push(u+p.slice(0,t)+e+p.slice(t)+o)}n+="(?:"+m.join("|")+")"+c[t]}}return n}));else{let e=s(A,o);2==n&&o>0&&(e=")("+e+")("),h=i.map(((t,n)=>'"'===t[0]?l(t.slice(1,-1)):t.split("").map(((e,t,l)=>(1==o&&0==t&&l.length>1&&e!=l[t+1]&&(e+="(?!"+e+")"),e))).join(e)+c[n]))}let p=2==g?a:"",d=2==f?a:"",m=d+s(e.interChars,e.interIns)+p;return n>0?r?h=p+"("+h.join(")"+d+"|"+p+"(")+")"+d:(h="("+h.join(")("+m+")(")+")",h="(.??"+p+")"+h+"("+d+".*)"):(h=h.join(m),h=p+h+d),[RegExp(h,"i"+E),i,c]},F=(e,t,l)=>{let[n]=T(t);if(null==n)return null;let r=[];if(null!=l)for(let t=0;l.length>t;t++){let i=l[t];n.test(e[i])&&r.push(i)}else for(let t=0;e.length>t;t++)n.test(e[t])&&r.push(t);return r};let O=!!L,v=RegExp(R,E),B=RegExp(L,E);const U=(t,l,n)=>{let[r,i,s]=T(n,1),[a]=T(n,2),h=i.length,u=t.length,c=Array(u).fill(0),o={idx:Array(u),start:c.slice(),chars:c.slice(),terms:c.slice(),interIns:c.slice(),intraIns:c.slice(),interLft2:c.slice(),interRgt2:c.slice(),interLft1:c.slice(),interRgt1:c.slice(),ranges:Array(u)},p=1==g||1==f,d=0;for(let n=0;t.length>n;n++){let u=l[t[n]],c=u.match(r),m=c.index+c[1].length,x=m,b=!1,R=0,L=0,A=0,S=0,I=0,z=0,C=0,y=0,k=[];for(let t=0,l=2;h>t;t++,l+=2){let n=c[l].toLocaleLowerCase(),r=i[t],a='"'==r[0]?r.slice(1,-1):r+s[t],o=a.length,d=n.length,j=n==a;if(!j&&c[l+1].length>=o){let e=c[l+1].toLocaleLowerCase().indexOf(a);e>-1&&(k.push(x,d,e,o),x+=N(c,l,e,o),n=a,d=o,j=!0,0==t&&(m=x))}if(p||j){let e=x-1,r=x+d,i=!1,s=!1;if(-1==e||v.test(u[e]))j&&R++,i=!0;else{if(2==g){b=!0;break}if(O&&B.test(u[e]+u[e+1]))j&&L++,i=!0;else if(1==g){let e=c[l+1],r=x+d;if(e.length>=o){let s,g=0,f=!1,h=RegExp(a,"ig"+E);for(;s=h.exec(e);){g=s.index;let e=r+g,t=e-1;if(-1==t||v.test(u[t])){R++,f=!0;break}if(B.test(u[t]+u[e])){L++,f=!0;break}}f&&(i=!0,k.push(x,d,g,o),x+=N(c,l,g,o),n=a,d=o,j=!0,0==t&&(m=x))}if(!i){b=!0;break}}}if(r==u.length||v.test(u[r]))j&&A++,s=!0;else{if(2==f){b=!0;break}if(O&&B.test(u[r-1]+u[r]))j&&S++,s=!0;else if(1==f){b=!0;break}}j&&(I+=o,i&&s&&z++)}if(d>o&&(y+=d-o),t>0&&(C+=c[l-1].length),!e.intraFilt(a,n,x)){b=!0;break}h-1>t&&(x+=d+c[l+1].length)}if(!b){o.idx[d]=t[n],o.interLft2[d]=R,o.interLft1[d]=L,o.interRgt2[d]=A,o.interRgt1[d]=S,o.chars[d]=I,o.terms[d]=z,o.interIns[d]=C,o.intraIns[d]=y,o.start[d]=m;let e=u.match(a),l=e.index+e[1].length,r=k.length,i=r>0?0:1/0,s=r-4;for(let t=2;e.length>t;)if(i>s||k[i]!=l)l+=e[t].length,t++;else{let n=k[i+1],r=k[i+2],s=k[i+3],a=t,g="";for(let t=0;n>t;a++)g+=e[a],t+=e[a].length;e.splice(t,a-t,g),l+=N(e,t,r,s),i+=4}l=e.index+e[1].length;let g=o.ranges[d]=[],f=l,h=l;for(let t=2;e.length>t;t++){let n=e[t].length;l+=n,t%2==0?h=l:n>0&&(g.push(f,h),f=h=l)}h>f&&g.push(f,h),d++}}if(t.length>d)for(let e in o)o[e]=o[e].slice(0,d);return o},N=(e,t,l,n)=>{let r=e[t]+e[t+1].slice(0,l);return e[t-1]+=r,e[t]=e[t+1].slice(l,l+n),e[t+1]=e[t+1].slice(l+n),r.length};return{search:(...t)=>((t,n,r,i=1e3,s)=>{r=r?!0===r?5:r:0;let a=null,g=null,f=[];n=n.replace(C,(e=>{let t=e.trim().slice(1);return'"'===t[0]&&(t=l(t.slice(1,-1))),f.push(t),""}));let u,c=M(n);if(f.length>0){if(u=RegExp(f.join("|"),"i"+E),0==c.length){let e=[];for(let l=0;t.length>l;l++)u.test(t[l])||e.push(l);return[e,null,null]}}else if(0==c.length)return[null,null,null];if(r>0){let e=M(n);if(e.length>1){let l=e.slice().sort(((e,t)=>t.length-e.length));for(let e=0;l.length>e;e++){if(0==s?.length)return[[],null,null];s=F(t,l[e],s)}if(e.length>r)return[s,null,null];a=h(e).map((e=>e.join(" "))),g=[];let n=new Set;for(let e=0;a.length>e;e++)if(s.length>n.size){let l=s.filter((e=>!n.has(e))),r=F(t,a[e],l);for(let e=0;r.length>e;e++)n.add(r[e]);g.push(r)}else g.push([])}}null==a&&(a=[n],g=[s?.length>0?s:F(t,n)]);let o=null,p=null;if(f.length>0&&(g=g.map((e=>e.filter((e=>!u.test(t[e])))))),i>=g.reduce(((e,t)=>e+t.length),0)){o={},p=[];for(let l=0;g.length>l;l++){let n=g[l];if(null==n||0==n.length)continue;let r=a[l],i=U(n,t,r),s=e.sort(i,t,r);if(l>0)for(let e=0;s.length>e;e++)s[e]+=p.length;for(let e in i)o[e]=(o[e]??[]).concat(i[e]);p=p.concat(s)}}return[[].concat(...g),o,p]})(...t),split:M,filter:F,info:U,sort:e.sort}}const f=(()=>{let e={A:"ÁÀÃÂÄĄ",a:"áàãâäą",E:"ÉÈÊËĖ",e:"éèêëę",I:"ÍÌÎÏĮ",i:"íìîïį",O:"ÓÒÔÕÖ",o:"óòôõö",U:"ÚÙÛÜŪŲ",u:"úùûüūų",C:"ÇČĆ",c:"çčć",L:"Ł",l:"ł",N:"ÑŃ",n:"ñń",S:"ŠŚ",s:"šś",Z:"ŻŹ",z:"żź"},t=new Map,l="";for(let n in e)e[n].split("").forEach((e=>{l+=e,t.set(e,n)}));let n=RegExp(`[${l}]`,"g"),r=e=>t.get(e);return e=>{if("string"==typeof e)return e.replace(n,r);let t=Array(e.length);for(let l=0;e.length>l;l++)t[l]=e[l].replace(n,r);return t}})();function h(e){let t,l,n=(e=e.slice()).length,r=[e.slice()],i=Array(n).fill(0),s=1;for(;n>s;)s>i[s]?(t=s%2&&i[s],l=e[s],e[s]=e[t],e[t]=l,++i[s],s=1,r.push(e.slice())):(i[s]=0,++s);return r}const u=(e,t)=>t?`${e}`:e,c=(e,t)=>e+t;return g.latinize=f,g.permute=e=>h([...Array(e.length).keys()]).sort(((e,t)=>{for(let l=0;e.length>l;l++)if(e[l]!=t[l])return e[l]-t[l];return 0})).map((t=>t.map((t=>e[t])))),g.highlight=function(e,t,l=u,n="",r=c){n=r(n,l(e.substring(0,t[0]),!1))??n;for(let i=0;t.length>i;i+=2)n=r(n,l(e.substring(t[i],t[i+1]),!0))??n,t.length-3>i&&(n=r(n,l(e.substring(t[i+1],t[i+2]),!1))??n);return r(n,l(e.substring(t[t.length-1]),!1))??n},g}(); diff --git a/src/main/app.ts b/src/main/app.ts index 6612414c..d5c851d9 100644 --- a/src/main/app.ts +++ b/src/main/app.ts @@ -17,8 +17,11 @@ import * as semver from 'semver'; import * as fs from 'fs'; import { clearSession, + EnvironmentInstallStatus, getBundledPythonEnvPath, getBundledPythonPath, + getNextPythonEnvName, + getPythonEnvsDirectory, installBundledEnvironment, isDarkTheme, waitForDuration @@ -49,12 +52,15 @@ import { EventTypeMain, EventTypeRenderer } from './eventtypes'; import { SettingsDialog } from './settingsdialog/settingsdialog'; import { AboutDialog } from './aboutdialog/aboutdialog'; import { AuthDialog } from './authdialog/authdialog'; +import { ManagePythonEnvironmentDialog } from './pythonenvdialog/pythonenvdialog'; +import { addUserSetEnvironment, createPythonEnvironment } from './cli'; export interface IApplication { createNewEmptySession(): void; createFreeServersIfNeeded(): void; checkForUpdates(showDialog: 'on-new-version' | 'always'): void; showSettingsDialog(activateTab?: SettingsDialog.Tab): void; + showManagePythonEnvsDialog(): void; showAboutDialog(): void; cliArgs: ICLIArguments; } @@ -399,6 +405,18 @@ export class JupyterApplication implements IApplication, IDisposable { dialog.load(); } + async showManagePythonEnvsDialog() { + const dialog = new ManagePythonEnvironmentDialog( + { + envs: await this._registry.getEnvironmentList(false), + isDarkTheme: this._isDarkTheme, + defaultPythonPath: userSettings.getValue(SettingType.pythonPath) + }, + this._registry + ); + dialog.load(); + } + closeSettingsDialog() { if (this._settingsDialog) { this._settingsDialog.window.close(); @@ -665,15 +683,18 @@ export class JupyterApplication implements IApplication, IDisposable { this._evm.registerEventHandler( EventTypeMain.InstallBundledPythonEnv, - async event => { - const installPath = getBundledPythonEnvPath(); + async (event, envPath: string) => { + const installPath = envPath || getBundledPythonEnvPath(); await installBundledEnvironment(installPath, { onInstallStatus: (status, message) => { event.sender.send( - EventTypeRenderer.InstallBundledPythonEnvStatus, + EventTypeRenderer.InstallPythonEnvStatus, status, message ); + if (status === EnvironmentInstallStatus.Success) { + addUserSetEnvironment(installPath, true); + } }, get forceOverwrite() { return false; @@ -703,6 +724,46 @@ export class JupyterApplication implements IApplication, IDisposable { } ); + this._evm.registerEventHandler( + EventTypeMain.ShowManagePythonEnvironmentsDialog, + async event => { + this.showManagePythonEnvsDialog(); + } + ); + + this._evm.registerSyncEventHandler( + EventTypeMain.GetNextPythonEnvironmentName, + (event, path) => { + return getNextPythonEnvName(); + } + ); + + this._evm.registerSyncEventHandler( + EventTypeMain.SelectPythonEnvInstallDirectory, + event => { + const currentPath = getPythonEnvsDirectory(); + + return new Promise((resolve, reject) => { + dialog + .showOpenDialog({ + properties: [ + 'openDirectory', + 'showHiddenFiles', + 'noResolveAliases', + 'createDirectory' + ], + buttonLabel: 'Use Directory', + defaultPath: currentPath + }) + .then(({ filePaths }) => { + if (filePaths.length > 0) { + resolve(filePaths[0]); + } + }); + }); + } + ); + this._evm.registerSyncEventHandler( EventTypeMain.ValidatePythonPath, (event, path) => { @@ -811,6 +872,36 @@ export class JupyterApplication implements IApplication, IDisposable { } ); + this._evm.registerEventHandler( + EventTypeMain.CreateNewPythonEnvironment, + async (event, envPath: string, envType: string, packages: string) => { + event.sender.send( + EventTypeRenderer.InstallPythonEnvStatus, + EnvironmentInstallStatus.Started + ); + try { + await createPythonEnvironment(envPath, envType, packages, { + stdout: (msg: string) => { + event.sender.send( + EventTypeRenderer.InstallPythonEnvStatus, + EnvironmentInstallStatus.Running, + msg + ); + } + }); + event.sender.send( + EventTypeRenderer.InstallPythonEnvStatus, + EnvironmentInstallStatus.Success + ); + } catch (error) { + event.sender.send( + EventTypeRenderer.InstallPythonEnvStatus, + EnvironmentInstallStatus.Failure + ); + } + } + ); + this._evm.registerSyncEventHandler( EventTypeMain.GetServerInfo, (event): Promise => { diff --git a/src/main/cli.ts b/src/main/cli.ts index 6b04b259..85a21f89 100644 --- a/src/main/cli.ts +++ b/src/main/cli.ts @@ -8,6 +8,7 @@ import { getBundledPythonPath, installBundledEnvironment, isBaseCondaEnv, + isEnvInstalledByDesktopApp, markEnvironmentAsJupyterInstalled, pythonPathForEnvPath } from './utils'; @@ -194,9 +195,7 @@ export async function handleEnvListCommand(argv: any) { name => `${name}: ${env.versions[name]}` ); const envPath = envPathForPythonPath(env.path); - const installedByApp = fs.existsSync( - path.join(envPath, '.jupyter', 'env.json') - ); + const installedByApp = isEnvInstalledByDesktopApp(envPath); listLines.push( ` [${env.name}], path: ${envPath}${ installedByApp ? ', installed by JupyterLab Desktop' : '' @@ -222,7 +221,7 @@ export async function handleEnvListCommand(argv: any) { console.log(listLines.join('\n')); } -function addUserSetEnvironment(envPath: string, isConda: boolean) { +export function addUserSetEnvironment(envPath: string, isConda: boolean) { const pythonPath = pythonPathForEnvPath(envPath, isConda); // this record will get updated with the correct data once app launches @@ -307,6 +306,50 @@ export async function handleEnvUpdateRegistryCommand(argv: any) { appData.save(); } +export async function createPythonEnvironment( + envPath: string, + envType: string, + packages: string, + callbacks?: ICommandRunCallbacks +) { + const isConda = envType === 'conda'; + const condaRootExists = isBaseCondaEnv(appData.condaRootPath); + + if (isConda) { + const createCommand = `conda create -y -c conda-forge -p ${envPath} ${packages}`; + await runCommandInEnvironment( + appData.condaRootPath, + createCommand, + callbacks + ); + } else { + if (condaRootExists) { + const createCommand = `python -m venv create ${envPath}`; + await runCommandInEnvironment( + appData.condaRootPath, + createCommand, + callbacks + ); + } else if (fs.existsSync(appData.systemPythonPath)) { + execFileSync(appData.systemPythonPath, ['-m', 'venv', 'create', envPath]); + } else { + throw { + message: + 'Failed to create Python environment. Python executable not found.' + }; + } + + if (packages.trim() !== '') { + const installCommand = `python -m pip install ${packages}`; + console.log('Installing packages...'); + await runCommandInEnvironment(envPath, installCommand, callbacks); + } + } + + markEnvironmentAsJupyterInstalled(envPath); + addUserSetEnvironment(envPath, isConda); +} + export async function handleEnvCreateCommand(argv: any) { const envPath = argv.path as string; @@ -353,33 +396,15 @@ export async function handleEnvCreateCommand(argv: any) { const createCondaEnv = isConda || (envType === 'auto' && condaRootExists); - if (createCondaEnv) { - const createCommand = `conda create -y -c conda-forge -p ${envPath} ${packageList.join( - ' ' - )}`; - await runCommandInEnvironment(appData.condaRootPath, createCommand); - } else { - if (condaRootExists) { - const createCommand = `python -m venv create ${envPath}`; - await runCommandInEnvironment(appData.condaRootPath, createCommand); - } else if (fs.existsSync(appData.systemPythonPath)) { - execFileSync(appData.systemPythonPath, ['-m', 'venv', 'create', envPath]); - } else { - console.error( - 'Failed to create Python environment. Python executable not found.' - ); - return; - } - - if (packageList.length > 0) { - const installCommand = `python -m pip install ${packageList.join(' ')}`; - console.log('Installing packages...'); - await runCommandInEnvironment(envPath, installCommand); - } + try { + await createPythonEnvironment( + envPath, + createCondaEnv ? 'conda' : 'venv', + packageList.join(' ') + ); + } catch (error) { + console.error(error); } - - markEnvironmentAsJupyterInstalled(envPath); - addUserSetEnvironment(envPath, createCondaEnv); } export async function handleEnvSetBaseCondaCommand(argv: any) { @@ -442,9 +467,19 @@ export async function launchCLIinEnvironment( }); } +export interface ICommandRunCallback { + (msg: string): void; +} + +export interface ICommandRunCallbacks { + stdout?: ICommandRunCallback; + stderr?: ICommandRunCallback; +} + export async function runCommandInEnvironment( envPath: string, - command: string + command: string, + callbacks?: ICommandRunCallbacks ) { const isWin = process.platform === 'win32'; const commandScript = createCommandScriptInEnv( @@ -471,12 +506,20 @@ export async function runCommandInEnvironment( if (shell.stdout) { shell.stdout.on('data', chunk => { - console.debug('>', Buffer.from(chunk).toString()); + const msg = Buffer.from(chunk).toString(); + console.debug('>', msg); + if (callbacks?.stdout) { + callbacks.stdout(msg); + } }); } if (shell.stderr) { shell.stderr.on('data', chunk => { - console.error('>', Buffer.from(chunk).toString()); + const msg = Buffer.from(chunk).toString(); + console.error('>', msg); + if (callbacks?.stdout) { + callbacks.stdout(msg); + } }); } diff --git a/src/main/eventtypes.ts b/src/main/eventtypes.ts index 5f39aa51..7fab6185 100644 --- a/src/main/eventtypes.ts +++ b/src/main/eventtypes.ts @@ -58,13 +58,18 @@ export enum EventTypeMain { SetAuthDialogResponse = 'set-auth-dialog-response', InstallPythonEnvRequirements = 'install-python-env-requirements', ShowLogs = 'show-logs', - CopyToClipboard = 'copy-to-clipboard' + CopyToClipboard = 'copy-to-clipboard', + GetNextPythonEnvironmentName = 'get-next-python-environment-name', + CreateNewPythonEnvironment = 'create-new-python-environment', + ShowManagePythonEnvironmentsDialog = 'show-manage-python-environments-dialog', + SelectPythonEnvInstallDirectory = 'select-python-environment-install-directory', + ShowPythonEnvironmentContextMenu = 'show-python-environment-context-menu' } // events sent to Renderer process export enum EventTypeRenderer { WorkingDirectorySelected = 'working-directory-selected', - InstallBundledPythonEnvStatus = 'install-bundled-python-env-status', + InstallPythonEnvStatus = 'install-python-env-status', CustomPythonPathSelected = 'custom-python-path-selected', ShowProgress = 'show-progress', SetCurrentPythonPath = 'set-current-python-path', @@ -77,5 +82,6 @@ export enum EventTypeRenderer { SetNewsList = 'set-news-list', SetNotificationMessage = 'set-notification-message', DisableLocalServerActions = 'disable-local-server-actions', - SetDefaultWorkingDirectoryResult = 'set-default-working-directory-result' + SetDefaultWorkingDirectoryResult = 'set-default-working-directory-result', + ResetPythonEnvSelectPopup = 'reset-python-env-select-popup' } diff --git a/src/main/main.ts b/src/main/main.ts index c1d60c18..15ceea26 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,7 +1,12 @@ import { app, Menu, MenuItem } from 'electron'; import log, { LevelOption } from 'electron-log'; import * as fs from 'fs'; -import { getAppDir, isDevMode, waitForFunction } from './utils'; +import { + getAppDir, + getPythonEnvsDirectory, + isDevMode, + waitForFunction +} from './utils'; import { execSync } from 'child_process'; import { JupyterApplication } from './app'; import { ICLIArguments } from './tokens'; @@ -136,6 +141,18 @@ function setupJLabCommand() { } } +function createPythonEnvsDirectory() { + const envsDir = getPythonEnvsDirectory(); + + try { + if (!fs.existsSync(envsDir)) { + fs.mkdirSync(envsDir, { recursive: true }); + } + } catch (error) { + log.error(error); + } +} + function setApplicationMenu() { if (process.platform !== 'darwin') { return; @@ -182,6 +199,7 @@ app.on('ready', async () => { redirectConsoleToLog(); setApplicationMenu(); setupJLabCommand(); + createPythonEnvsDirectory(); argv.cwd = process.cwd(); jupyterApp = new JupyterApplication((argv as unknown) as ICLIArguments); } catch (error) { diff --git a/src/main/progressview/preload.ts b/src/main/progressview/preload.ts index 727b6978..1a261097 100644 --- a/src/main/progressview/preload.ts +++ b/src/main/progressview/preload.ts @@ -50,7 +50,7 @@ ipcRenderer.on( ); ipcRenderer.on( - EventTypeRenderer.InstallBundledPythonEnvStatus, + EventTypeRenderer.InstallPythonEnvStatus, (event, result, message) => { if (onInstallBundledPythonEnvStatusListener) { onInstallBundledPythonEnvStatusListener(result, message); diff --git a/src/main/pythonenvdialog/preload.ts b/src/main/pythonenvdialog/preload.ts new file mode 100644 index 00000000..a9476cb1 --- /dev/null +++ b/src/main/pythonenvdialog/preload.ts @@ -0,0 +1,160 @@ +import { EventTypeMain, EventTypeRenderer } from '../eventtypes'; + +const { contextBridge, ipcRenderer } = require('electron'); + +type InstallBundledPythonEnvStatusListener = ( + status: string, + msg: string +) => void; +type CustomPythonPathSelectedListener = (path: string) => void; +type WorkingDirectorySelectedListener = (path: string) => void; + +let onInstallBundledPythonEnvStatusListener: InstallBundledPythonEnvStatusListener; +let onCustomPythonPathSelectedListener: CustomPythonPathSelectedListener; +let onWorkingDirectorySelectedListener: WorkingDirectorySelectedListener; + +contextBridge.exposeInMainWorld('electronAPI', { + getAppConfig: () => { + return { + platform: process.platform + }; + }, + isDarkTheme: () => { + return ipcRenderer.invoke(EventTypeMain.IsDarkTheme); + }, + restartApp: () => { + ipcRenderer.send(EventTypeMain.RestartApp); + }, + getNextPythonEnvironmentName: () => { + return ipcRenderer.invoke(EventTypeMain.GetNextPythonEnvironmentName); + }, + createNewPythonEnvironment: ( + envPath: string, + envType: string, + packages: string + ) => { + ipcRenderer.send( + EventTypeMain.CreateNewPythonEnvironment, + envPath, + envType, + packages + ); + }, + selectPythonEnvInstallDirectory: () => { + return ipcRenderer.invoke(EventTypeMain.SelectPythonEnvInstallDirectory); + }, + showPythonEnvironmentContextMenu: (pythonPath: string) => { + ipcRenderer.send( + EventTypeMain.ShowPythonEnvironmentContextMenu, + pythonPath + ); + }, + + setCheckForUpdatesAutomatically: (check: boolean) => { + ipcRenderer.send(EventTypeMain.SetCheckForUpdatesAutomatically, check); + }, + setInstallUpdatesAutomatically: (install: boolean) => { + ipcRenderer.send(EventTypeMain.SetInstallUpdatesAutomatically, install); + }, + checkForUpdates: () => { + ipcRenderer.send(EventTypeMain.CheckForUpdates); + }, + showLogs: () => { + ipcRenderer.send(EventTypeMain.ShowLogs); + }, + launchInstallerDownloadPage: () => { + ipcRenderer.send(EventTypeMain.LaunchInstallerDownloadPage); + }, + setStartupMode: (mode: string) => { + ipcRenderer.send(EventTypeMain.SetStartupMode, mode); + }, + setTheme: (theme: string) => { + ipcRenderer.send(EventTypeMain.SetTheme, theme); + }, + setSyncJupyterLabTheme: (sync: boolean) => { + ipcRenderer.send(EventTypeMain.SetSyncJupyterLabTheme, sync); + }, + setShowNewsFeed: (show: string) => { + ipcRenderer.send(EventTypeMain.SetShowNewsFeed, show); + }, + selectWorkingDirectory: () => { + ipcRenderer.send(EventTypeMain.SelectWorkingDirectory); + }, + onWorkingDirectorySelected: (callback: WorkingDirectorySelectedListener) => { + onWorkingDirectorySelectedListener = callback; + }, + setDefaultWorkingDirectory: (path: string) => { + ipcRenderer.send(EventTypeMain.SetDefaultWorkingDirectory, path); + }, + installBundledPythonEnv: (envPath: string) => { + ipcRenderer.send(EventTypeMain.InstallBundledPythonEnv, envPath); + }, + updateBundledPythonEnv: () => { + ipcRenderer.send(EventTypeMain.InstallBundledPythonEnv); + }, + onInstallBundledPythonEnvStatus: ( + callback: InstallBundledPythonEnvStatusListener + ) => { + onInstallBundledPythonEnvStatusListener = callback; + }, + selectPythonPath: () => { + ipcRenderer.send(EventTypeMain.SelectPythonPath); + }, + onCustomPythonPathSelected: (callback: CustomPythonPathSelectedListener) => { + onCustomPythonPathSelectedListener = callback; + }, + setDefaultPythonPath: (path: string) => { + ipcRenderer.send(EventTypeMain.SetDefaultPythonPath, path); + }, + validatePythonPath: (path: string) => { + return ipcRenderer.invoke(EventTypeMain.ValidatePythonPath, path); + }, + showInvalidPythonPathMessage: (path: string) => { + ipcRenderer.send(EventTypeMain.ShowInvalidPythonPathMessage, path); + }, + clearHistory: (options: any) => { + return ipcRenderer.invoke(EventTypeMain.ClearHistory, options); + }, + setLogLevel: (level: string) => { + ipcRenderer.send(EventTypeMain.SetLogLevel, level); + }, + setServerLaunchArgs: ( + serverArgs: string, + overrideDefaultServerArgs: boolean + ) => { + ipcRenderer.send( + EventTypeMain.SetServerLaunchArgs, + serverArgs, + overrideDefaultServerArgs + ); + }, + setServerEnvVars: (serverEnvVars: any) => { + ipcRenderer.send(EventTypeMain.SetServerEnvVars, serverEnvVars); + }, + setCtrlWBehavior: (behavior: string) => { + ipcRenderer.send(EventTypeMain.SetCtrlWBehavior, behavior); + } +}); + +ipcRenderer.on(EventTypeRenderer.WorkingDirectorySelected, (event, path) => { + if (onWorkingDirectorySelectedListener) { + onWorkingDirectorySelectedListener(path); + } +}); + +ipcRenderer.on( + EventTypeRenderer.InstallPythonEnvStatus, + (event, result, msg) => { + if (onInstallBundledPythonEnvStatusListener) { + onInstallBundledPythonEnvStatusListener(result, msg); + } + } +); + +ipcRenderer.on(EventTypeRenderer.CustomPythonPathSelected, (event, path) => { + if (onCustomPythonPathSelectedListener) { + onCustomPythonPathSelectedListener(path); + } +}); + +export {}; diff --git a/src/main/pythonenvdialog/pythonenvdialog.ts b/src/main/pythonenvdialog/pythonenvdialog.ts new file mode 100644 index 00000000..a43267cf --- /dev/null +++ b/src/main/pythonenvdialog/pythonenvdialog.ts @@ -0,0 +1,891 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import * as ejs from 'ejs'; +import { + app, + BrowserWindow, + clipboard, + Menu, + MenuItemConstructorOptions +} from 'electron'; +import * as path from 'path'; +import * as fs from 'fs'; +import { ThemedWindow } from '../dialog/themedwindow'; +import { IRegistry } from '../registry'; +import { IEnvironmentType, IPythonEnvironment } from '../tokens'; +import { + envPathForPythonPath, + getBundledPythonPath, + getCondaPath, + getNextPythonEnvName, + getPythonEnvsDirectory, + isEnvInstalledByDesktopApp, + versionWithoutSuffix +} from '../utils'; +import { EventManager } from '../eventmanager'; +import { EventTypeMain } from '../eventtypes'; + +export class ManagePythonEnvironmentDialog { + constructor( + options: ManagePythonEnvironmentDialog.IOptions, + registry: IRegistry + ) { + this._registry = registry; + this._window = new ThemedWindow({ + isDarkTheme: options.isDarkTheme, + title: 'Manage Python environments', + width: 800, + height: 500, + preload: path.join(__dirname, './preload.js') + }); + + let defaultPythonPath = options.defaultPythonPath; + const bundledPythonPath = getBundledPythonPath(); + + if (defaultPythonPath === '') { + defaultPythonPath = bundledPythonPath; + } + let bundledEnvInstallationExists = false; + try { + bundledEnvInstallationExists = fs.existsSync(bundledPythonPath); + } catch (error) { + console.error('Failed to check for bundled Python path', error); + } + + const selectBundledPythonPath = + (defaultPythonPath === '' || defaultPythonPath === bundledPythonPath) && + bundledEnvInstallationExists; + + let bundledEnvInstallationLatest = true; + + if (bundledEnvInstallationExists) { + try { + const bundledEnv = registry.getEnvironmentByPath(bundledPythonPath); + const jlabVersion = bundledEnv.versions['jupyterlab']; + const appVersion = app.getVersion(); + + if ( + versionWithoutSuffix(jlabVersion) !== versionWithoutSuffix(appVersion) + ) { + bundledEnvInstallationLatest = false; + } + } catch (error) { + console.error('Failed to check bundled environment update', error); + } + } + + const condaEnvs: IPythonEnvironment[] = options.envs.filter( + env => + env.type === IEnvironmentType.CondaEnv || + env.type === IEnvironmentType.CondaRoot + ); + const venvEnvs: IPythonEnvironment[] = options.envs.filter( + env => env.type === IEnvironmentType.VirtualEnv + ); + const globalEnvs: IPythonEnvironment[] = options.envs.filter( + env => env.type === IEnvironmentType.Path + ); + + const infoIconSrc = fs.readFileSync( + path.join(__dirname, '../../../app-assets/info-icon.svg') + ); + const menuIconSrc = fs.readFileSync( + path.join(__dirname, '../../../app-assets/ellipsis-vertical.svg') + ); + + const pythonEnvName = getNextPythonEnvName(); + const pythonEnvInstallPath = getPythonEnvsDirectory(); + const condaPath = getCondaPath(); + + this._evm.registerEventHandler( + EventTypeMain.ShowPythonEnvironmentContextMenu, + async (event, pythonPath) => { + const envPath = envPathForPythonPath(pythonPath); + const installedByApp = isEnvInstalledByDesktopApp(envPath); + const template: MenuItemConstructorOptions[] = [ + { + label: 'Copy Python path', + click: () => { + clipboard.writeText(pythonPath); + } + }, + { + label: 'Copy environment info', + click: () => { + const env = this._registry.getEnvironmentByPath(pythonPath); + if (env) { + clipboard.writeText( + JSON.stringify({ + pythonPath: env.path, + name: env.name, + type: env.type, + versions: env.versions, + defaultKernel: env.defaultKernel + }) + ); + } else { + clipboard.writeText('Failed to get environment info!'); + } + } + }, + { type: 'separator', visible: installedByApp }, + { + label: 'Delete', + visible: installedByApp + // click: () => { + // this._app.showSettingsDialog(); + // } + } + ]; + + const menu = Menu.buildFromTemplate(template); + menu.popup({ + window: BrowserWindow.fromWebContents(event.sender) + }); + } + ); + + const envTypeTemplate = ` + <% + function getEnvTooltip(env) { + const packages = []; + for (const name in env.versions) { + packages.push(name + ': ' + env.versions[name]); + } + return env.name + '\\n' + env.path + '\\n' + packages.join(', '); + } + %> +
<%- envType %> (<%- envs.length %>)
+ <% envs.forEach(env => { %> + +
<%- env.path %>
+
<%- env.name %>
${menuIconSrc}
+
+ <% }); %> + `; + + const template = ` + +
+ + Environments + Create new + Settings + + + Available Python environments +
+
+ Add existing + Create new +
+
+ +
+ + <%- condaEnvs %> + <%- venvEnvs %> + <%- globalEnvs %> + +
+
+ + +
+
+
+ Create +
+
+ + Copy of the bundled environment + New environment + +
+
+ +
+
+ Name
${infoIconSrc}
+
+
+ +
+
+ +
+
+ +
+
+ Environment type +
+
+ + conda + venv + +
+
+ +
+
+ Python packages to install +
+
+ Include jupyterlab (required for use in JupyterLab Desktop) +
+
+ Additional Python packages +
+
+ + +
+
+ +
+
+ Environment create command preview +
+
+ +
+
+ +
+
+ Create +
+
+ Show output + Clear form +
+
+ +
+
+ +
+
+
+
+ + + Python environment settings + +
+
+ Default Python path for JupyterLab Server
${infoIconSrc}
+
+
+
+
InstallUpdate
+ + <%= !bundledEnvInstallationExists ? 'disabled' : '' %> onchange="handleDefaultPythonEnvTypeChange(this);">Use bundled Python environment installation + onchange="handleDefaultPythonEnvTypeChange(this);">Use custom Python environment + + +
+
+ +
+
+ Select path +
+
+
+
+
+ +
+
+ New Python environment install path
${infoIconSrc}
+
+
+
+ +
+
+ Select path +
+
+
+ +
+
+ conda path
${infoIconSrc}
+
+
+
+ +
+
+ Select path +
+
+
+ +
+
+ Python path
${infoIconSrc}
+
+
+
+ +
+
+ Select path +
+
+
+
+
+ +
+ + `; + this._pageBody = ejs.render(template, { + condaEnvs: ejs.render(envTypeTemplate, { + envType: 'conda', + envs: condaEnvs + }), + venvEnvs: ejs.render(envTypeTemplate, { + envType: 'venv', + envs: venvEnvs + }), + globalEnvs: ejs.render(envTypeTemplate, { + envType: 'global', + envs: globalEnvs + }), + defaultPythonPath, + selectBundledPythonPath, + bundledEnvInstallationExists, + bundledEnvInstallationLatest, + pythonEnvName, + pythonEnvInstallPath, + condaPath + }); + } + + get window(): BrowserWindow { + return this._window.window; + } + + load() { + this._window.loadDialogContent(this._pageBody); + this._window.window.on('close', () => { + this._evm.dispose(); + }); + } + + private _window: ThemedWindow; + private _pageBody: string; + private _evm = new EventManager(); + private _registry: IRegistry; +} + +export namespace ManagePythonEnvironmentDialog { + export interface IOptions { + isDarkTheme: boolean; + envs: IPythonEnvironment[]; + defaultPythonPath: string; + } +} diff --git a/src/main/pythonenvselectpopup/preload.ts b/src/main/pythonenvselectpopup/preload.ts index dfd59940..3b3b81a4 100644 --- a/src/main/pythonenvselectpopup/preload.ts +++ b/src/main/pythonenvselectpopup/preload.ts @@ -3,10 +3,12 @@ import { EventTypeMain, EventTypeRenderer } from '../eventtypes'; const { contextBridge, ipcRenderer } = require('electron'); type CurrentPythonPathSetListener = (path: string) => void; +type ResetPythonEnvSelectPopupListener = () => void; type CustomPythonPathSelectedListener = (path: string) => void; let onCustomPythonPathSelectedListener: CustomPythonPathSelectedListener; let onCurrentPythonPathSetListener: CurrentPythonPathSetListener; +let onResetPythonEnvSelectPopupListener: ResetPythonEnvSelectPopupListener; contextBridge.exposeInMainWorld('electronAPI', { getAppConfig: () => { @@ -17,6 +19,9 @@ contextBridge.exposeInMainWorld('electronAPI', { isDarkTheme: () => { return ipcRenderer.invoke(EventTypeMain.IsDarkTheme); }, + showManagePythonEnvsDialog: () => { + ipcRenderer.send(EventTypeMain.ShowManagePythonEnvironmentsDialog); + }, browsePythonPath: (currentPath: string) => { ipcRenderer.send(EventTypeMain.SelectPythonPath, currentPath); }, @@ -26,6 +31,11 @@ contextBridge.exposeInMainWorld('electronAPI', { onCurrentPythonPathSet: (callback: CurrentPythonPathSetListener) => { onCurrentPythonPathSetListener = callback; }, + onResetPythonEnvSelectPopup: ( + callback: ResetPythonEnvSelectPopupListener + ) => { + onResetPythonEnvSelectPopupListener = callback; + }, onCustomPythonPathSelected: (callback: CustomPythonPathSelectedListener) => { onCustomPythonPathSelectedListener = callback; }, @@ -40,6 +50,12 @@ ipcRenderer.on(EventTypeRenderer.SetCurrentPythonPath, (event, path) => { } }); +ipcRenderer.on(EventTypeRenderer.ResetPythonEnvSelectPopup, event => { + if (onResetPythonEnvSelectPopupListener) { + onResetPythonEnvSelectPopupListener(); + } +}); + ipcRenderer.on(EventTypeRenderer.CustomPythonPathSelected, (event, path) => { if (onCustomPythonPathSelectedListener) { onCustomPythonPathSelectedListener(path); diff --git a/src/main/pythonenvselectpopup/pythonenvselectpopup.ts b/src/main/pythonenvselectpopup/pythonenvselectpopup.ts index ca832e50..f7e52a2a 100644 --- a/src/main/pythonenvselectpopup/pythonenvselectpopup.ts +++ b/src/main/pythonenvselectpopup/pythonenvselectpopup.ts @@ -3,6 +3,7 @@ import * as ejs from 'ejs'; import * as path from 'path'; +import * as fs from 'fs'; import { ThemedView } from '../dialog/themedview'; import { EventTypeRenderer } from '../eventtypes'; import { IPythonEnvironment } from '../tokens'; @@ -18,6 +19,10 @@ export class PythonEnvironmentSelectPopup { this._envs = options.envs; const currentPythonPath = options.currentPythonPath || ''; + const uFuzzyScriptSrc = fs.readFileSync( + path.join(__dirname, '../../../app-assets/uFuzzy.iife.min.js') + ); + const template = ` +
+
+ Restart session using a different Python environment +
- + - +
- <% if (envs.length > 0) { %> - <% - function getEnvTooltip(env) { - const packages = []; - for (const name in env.versions) { - packages.push(name + ': ' + env.versions[name]); - } - return env.name + '\\n' + env.path + '\\n' + packages.join(', '); - } - %>
- - <% envs.forEach(env => { %> - <%- env.path %> -
<%- env.name %><%- env.path === ${JSON.stringify( - defaultPythonPath - )} ? ' (default)' : env.path === ${JSON.stringify( - bundledPythonPath - )} ? ' (bundled)' : '' %>
-
- <% }); %> -
+
- <% } %>
`; @@ -209,6 +288,12 @@ export class PythonEnvironmentSelectPopup { ); } + resetView() { + this._view.view.webContents.send( + EventTypeRenderer.ResetPythonEnvSelectPopup + ); + } + getScrollHeight(): number { const envCount = this._envs.length; return ( diff --git a/src/main/sessionwindow/sessionwindow.ts b/src/main/sessionwindow/sessionwindow.ts index 1341684f..8cc6436b 100644 --- a/src/main/sessionwindow/sessionwindow.ts +++ b/src/main/sessionwindow/sessionwindow.ts @@ -868,6 +868,12 @@ export class SessionWindow implements IDisposable { this._app.showSettingsDialog(); } }, + { + label: 'Manage Python environments', + click: () => { + this._app.showManagePythonEnvsDialog(); + } + }, { label: 'Check for updates…', click: () => { @@ -1143,7 +1149,9 @@ export class SessionWindow implements IDisposable { } } + // TODO: bug. this._envSelectPopup might still be loading this._envSelectPopup.setCurrentPythonPath(currentPythonPath); + this._envSelectPopup.resetView(); this._window.addBrowserView(this._envSelectPopup.view.view); this._envSelectPopupVisible = true; diff --git a/src/main/settingsdialog/preload.ts b/src/main/settingsdialog/preload.ts index c90b356b..9cb967d8 100644 --- a/src/main/settingsdialog/preload.ts +++ b/src/main/settingsdialog/preload.ts @@ -114,14 +114,11 @@ ipcRenderer.on(EventTypeRenderer.WorkingDirectorySelected, (event, path) => { } }); -ipcRenderer.on( - EventTypeRenderer.InstallBundledPythonEnvStatus, - (event, result) => { - if (onInstallBundledPythonEnvStatusListener) { - onInstallBundledPythonEnvStatusListener(result); - } +ipcRenderer.on(EventTypeRenderer.InstallPythonEnvStatus, (event, result) => { + if (onInstallBundledPythonEnvStatusListener) { + onInstallBundledPythonEnvStatusListener(result); } -); +}); ipcRenderer.on(EventTypeRenderer.CustomPythonPathSelected, (event, path) => { if (onCustomPythonPathSelectedListener) { diff --git a/src/main/utils.ts b/src/main/utils.ts index 9cb52cff..8596f70f 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -93,6 +93,40 @@ export function getBundledPythonPath(): string { return pythonPathForEnvPath(getBundledPythonEnvPath(), true); } +export function getPythonEnvsDirectory(): string { + // TODO: user settings + const userDataDir = getBundledPythonInstallDir(); + + return path.join(userDataDir, 'envs'); +} + +export function getNextPythonEnvName(): string { + const envsDir = getPythonEnvsDirectory(); + const prefix = 'env_'; + const maxTries = 10000; + + let index = 1; + const getNextName = () => { + return `${prefix}${index++}`; + }; + + let name = getNextName(); + + while (fs.existsSync(path.join(envsDir, name))) { + if (index > maxTries) { + return 'invalid_env'; + } + name = getNextName(); + } + + return name; +} + +export function getCondaPath() { + // user settings + return process.env['CONDA_EXE']; +} + export function isDarkTheme(themeType: string) { if (themeType === 'light') { return false; @@ -225,6 +259,7 @@ export function versionWithoutSuffix(version: string) { export enum EnvironmentInstallStatus { Started = 'STARTED', + Running = 'RUNNING', Failure = 'FAILURE', Cancelled = 'CANCELLED', Success = 'SUCCESS', @@ -404,6 +439,10 @@ export function jupyterEnvInstallInfoPathForEnvPath(envPath: string) { return path.join(envPath, '.jupyter', 'env.json'); } +export function isEnvInstalledByDesktopApp(envPath: string) { + return fs.existsSync(jupyterEnvInstallInfoPathForEnvPath(envPath)); +} + export function isCondaEnv(envPath: string): boolean { return fs.existsSync(path.join(envPath, 'conda-meta')); } diff --git a/src/main/welcomeview/preload.ts b/src/main/welcomeview/preload.ts index 04be86e6..27882d11 100644 --- a/src/main/welcomeview/preload.ts +++ b/src/main/welcomeview/preload.ts @@ -114,7 +114,7 @@ ipcRenderer.on(EventTypeRenderer.DisableLocalServerActions, event => { }); ipcRenderer.on( - EventTypeRenderer.InstallBundledPythonEnvStatus, + EventTypeRenderer.InstallPythonEnvStatus, (event, result, message) => { if (onInstallBundledPythonEnvStatusListener) { onInstallBundledPythonEnvStatusListener(result, message); diff --git a/webpack.preload.js b/webpack.preload.js index 689cc376..c0a60f7c 100644 --- a/webpack.preload.js +++ b/webpack.preload.js @@ -14,6 +14,7 @@ const preloadFiles = [ 'labview/preload.js', 'settingsdialog/preload.js', 'progressview/preload.js', + 'pythonenvdialog/preload.js', 'pythonenvselectpopup/preload.js', 'remoteserverselectdialog/preload.js', 'titlebarview/preload.js', diff --git a/yarn.lock b/yarn.lock index c2f7d2e3..b20ba0c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -200,6 +200,11 @@ "@microsoft/fast-foundation" "^2.37.1" "@microsoft/fast-web-utilities" "^5.1.0" +"@leeoniya/ufuzzy@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@leeoniya/ufuzzy/-/ufuzzy-1.0.14.tgz#01572c0de9cfa1420cf6ecac76dd59db5ebd1337" + integrity sha512-/xF4baYuCQMo+L/fMSUrZnibcu0BquEGnbxfVPiZhs/NbJeKj4c/UmFpQzW9Us0w45ui/yYW3vyaqawhNYsTzA== + "@lumino/algorithm@^1.9.2": version "1.9.2" resolved "https://registry.yarnpkg.com/@lumino/algorithm/-/algorithm-1.9.2.tgz#b95e6419aed58ff6b863a51bfb4add0f795141d3" @@ -2067,6 +2072,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" @@ -2150,7 +2160,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^7.1.1, glob@^7.1.3, glob@^7.1.6: +glob@^7.0.0, glob@^7.1.1, glob@^7.1.3, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -2301,6 +2311,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" @@ -2436,6 +2453,11 @@ internal-slot@^1.0.3, internal-slot@^1.0.4: has "^1.0.3" side-channel "^1.0.4" +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + interpret@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" @@ -2482,6 +2504,13 @@ is-ci@^3.0.0: dependencies: ci-info "^3.2.0" +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + is-core-module@^2.5.0, is-core-module@^2.9.0: version "2.11.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" @@ -2971,7 +3000,7 @@ minimist-options@4.1.0, minimist-options@^4.0.2: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.2.6: +minimist@^1.2.3, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -3520,6 +3549,13 @@ readdir-scoped-modules@^1.0.0: graceful-fs "^4.1.2" once "^1.3.0" +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== + dependencies: + resolve "^1.1.6" + rechoir@^0.7.0: version "0.7.1" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" @@ -3589,6 +3625,15 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve@^1.1.6: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^1.10.0, resolve@^1.9.0: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" @@ -3768,6 +3813,23 @@ shell-path@^2.1.0: dependencies: shell-env "^0.3.0" +shelljs@^0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +shx@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.4.tgz#74289230b4b663979167f94e1935901406e40f02" + integrity sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g== + dependencies: + minimist "^1.2.3" + shelljs "^0.8.5" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" From bd2718c23bb3d21b059fce93f5f2a742f56adc0c Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Tue, 26 Dec 2023 19:45:25 -0800 Subject: [PATCH 02/62] add linux aarch64 support --- env_installer/conda-linux-aarch64.lock | 218 +++++++++++++++++++++++++ env_installer/jlab_server.yaml | 1 + package.json | 1 + 3 files changed, 220 insertions(+) create mode 100644 env_installer/conda-linux-aarch64.lock diff --git a/env_installer/conda-linux-aarch64.lock b/env_installer/conda-linux-aarch64.lock new file mode 100644 index 00000000..9663128c --- /dev/null +++ b/env_installer/conda-linux-aarch64.lock @@ -0,0 +1,218 @@ +# Generated by conda-lock. +# platform: linux-aarch64 +# input_hash: 53fb1e47ee6c018d44ee71ce2c3bfee3906cfc93e84dbba64883a380f733a950 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-aarch64/ca-certificates-2023.11.17-hcefe29a_0.conda#695a28440b58e3ba920bcac4ac7c73c6 +https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.40-h2d8c526_0.conda#16246d69e945d0b1969a6099e7c5d457 +https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-13.2.0-hf8544c7_3.conda#191eb9058c6e97ca5fea3552e348a237 +https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-13.2.0-h9a76618_3.conda#7ad2164936c4975d94ca883d34809c0f +https://conda.anaconda.org/conda-forge/noarch/pybind11-abi-4-hd8ed1ab_3.tar.bz2#878f923dd6acc8aeb47a75da6c4098be +https://conda.anaconda.org/conda-forge/linux-aarch64/python_abi-3.8-4_cp38.conda#8107237a22bf62f12b4a7c8e0fa3b842 +https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2#6168d71addc746e8f2b8d57dfd2edcea +https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-13.2.0-hf8544c7_3.conda#00f021ee1a24c798ae53c87ee79597f1 +https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h31becfc_5.conda#a64e35f01e0b7a2a152eca87d33b9c87 +https://conda.anaconda.org/conda-forge/linux-aarch64/c-ares-1.24.0-h31becfc_0.conda#29763ca997f793dc5e0f2a24844c8502 +https://conda.anaconda.org/conda-forge/linux-aarch64/fmt-10.1.1-h2a328a1_1.conda#49695e320c2a672846a90ca623dce1da +https://conda.anaconda.org/conda-forge/linux-aarch64/icu-73.2-h787c7f5_0.conda#9d3c29d71f28452a2e843aff8cbe09d2 +https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.1-h4e544f5_0.tar.bz2#1f24853e59c68892452ef94ddd8afd4b +https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.0.0-h4de3ea5_0.tar.bz2#1a0ffc65e03ce81559dbcb0695ad1476 +https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlicommon-1.1.0-h31becfc_1.conda#1b219fd801eddb7a94df5bd001053ad9 +https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.19-h31becfc_0.conda#014e57e35f2dc95c9a12f63d4378e093 +https://conda.anaconda.org/conda-forge/linux-aarch64/libev-4.33-h31becfc_2.conda#a9a13cb143bbaa477b1ebaefbe47a302 +https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.4.2-h3557bc0_5.tar.bz2#dddd85f4d52121fab0a8b099c5e06501 +https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran5-13.2.0-h582850c_3.conda#d81dcb787465447542ad9c4cf0bab65e +https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.17-h31becfc_2.conda#9a8eb13f14de7d761555a98712e6df65 +https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.0.0-h31becfc_1.conda#ed24e702928be089d9ba3f05618515c6 +https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h31becfc_0.conda#c14f32510f694e3185704d89967ec422 +https://conda.anaconda.org/conda-forge/linux-aarch64/libsodium-1.0.18-hb9de7d4_1.tar.bz2#d09ab3c60eebb6f14eb4d07e172775cc +https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.38.1-hb4cce97_0.conda#000e30b09db0b7c775b21695dff30969 +https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.3.2-h31becfc_0.conda#1490de434d2a2c06a98af27641a2ffff +https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda#b4df5d7d4b63579d081fd3a4cf99740e +https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.2.13-h31becfc_5.conda#b213aa87eea9491ef7b129179322e955 +https://conda.anaconda.org/conda-forge/linux-aarch64/lz4-c-1.9.4-hd600fc2_0.conda#500145a83ed07ce79c8cef24252f366b +https://conda.anaconda.org/conda-forge/linux-aarch64/lzo-2.10-h516909a_1000.tar.bz2#ef5661339990c399c68c71cfb341e6d7 +https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.4-h0425590_2.conda#4ff0a396150dedad4269e16e5810f769 +https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.2.0-h31becfc_1.conda#b24247441ed7ce138382de2ec51200e4 +https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-hb9de7d4_1001.tar.bz2#d0183ec6ce0b5aaa3486df25fa5f0ded +https://conda.anaconda.org/conda-forge/linux-aarch64/reproc-14.2.4.post0-h31becfc_1.conda#c148bb4ba029a018527d3e4d5c7b63fa +https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.11-h31becfc_0.conda#13de34f69cb73165dbe08c1e9148bedb +https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.3-h3557bc0_0.tar.bz2#a6c9016ae1ca5c47a3603ed4cd65fedd +https://conda.anaconda.org/conda-forge/linux-aarch64/xz-5.2.6-h9cdd2b7_0.tar.bz2#83baad393a31d59c20b63ba4da6592df +https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-0.2.5-hf897c2e_2.tar.bz2#b853307650cb226731f653aa623936a4 +https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-cpp-0.8.0-h2f0025b_0.conda#b5da38ee183c1e50e3e7ffb171a2eca5 +https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlidec-1.1.0-h31becfc_1.conda#8db7cff89510bec0b863a0a8ee6a7bce +https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlienc-1.1.0-h31becfc_1.conda#ad3d3a826b5848d99936e4466ebbaa26 +https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20191231-he28a2e2_2.tar.bz2#29371161d77933a54fccf1bb66b96529 +https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran-ng-13.2.0-he9431aa_3.conda#6c292066bb9876d7ba35c590868baaeb +https://conda.anaconda.org/conda-forge/linux-aarch64/libnghttp2-1.58.0-hb0e430d_1.conda#8f724cdddffa79152de61f5564a3526b +https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.39-hf9034f9_0.conda#5ec9052384a6ac85e9111e9ac7c5ec4c +https://conda.anaconda.org/conda-forge/linux-aarch64/libsolv-0.7.27-hd84c7bf_0.conda#7e092bca53956dd2fddb1eed62c22c29 +https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.44.2-h194ca79_0.conda#464a0dedd1131669324946ee1c13c1a5 +https://conda.anaconda.org/conda-forge/linux-aarch64/libssh2-1.11.0-h492db2e_0.conda#45532845e121677ad328c9af9953f161 +https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.15-h2a766a3_0.conda#eb3d8c8170e3d03f2564ed2024aa00c8 +https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.12.3-h3091e33_0.conda#f7582eb42bcaa458503819687da6a143 +https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8fc344f_1.conda#105eb1e16bf83bfb2eb380a48032b655 +https://conda.anaconda.org/conda-forge/linux-aarch64/reproc-cpp-14.2.4.post0-h2f0025b_1.conda#35148ef0f190022ca52cf6edd6bdc814 +https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-h194ca79_0.conda#f75105e0585851f818e0009dd1dde4dc +https://conda.anaconda.org/conda-forge/linux-aarch64/zeromq-4.3.5-h2f0025b_0.conda#88905c542167163a0dea6bdad01c3366 +https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.5-h4c53e97_0.conda#b74eb9dbb5c3c15cb3cee7cbdf198c75 +https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-bin-1.1.0-h31becfc_1.conda#9e4a13596ab651ea8d77aae023d0ce3f +https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.12.1-hf0a5ef3_2.conda#a5ab74c5bd158c3d5532b66d8d83d907 +https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.21.2-hc419048_0.conda#55b51af37bf6fdcfe06f140e62e8c8db +https://conda.anaconda.org/conda-forge/linux-aarch64/libarchive-3.7.2-hd2f85e0_1.conda#a0f2e7adbcdf4041d6ee273d07ca171e +https://conda.anaconda.org/conda-forge/linux-aarch64/libopenblas-0.3.25-pthreads_h5a5ec62_0.conda#60e86bc93e3f213278dc5081115fb63b +https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.6.0-h1708d11_2.conda#d5638e110e7f22e2602a8edd20656720 +https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.8.18-hbbe8eec_1_cpython.conda#c070aab88cfec2d531762b134a9f6c13 +https://conda.anaconda.org/conda-forge/noarch/archspec-0.2.2-pyhd8ed1ab_0.conda#0dc2fce00a160271714647c019e3a8a8 +https://conda.anaconda.org/conda-forge/noarch/attrs-23.1.0-pyh71513ae_1.conda#3edfead7cedd1ab4400a6c588f3e75f8 +https://conda.anaconda.org/conda-forge/noarch/backcall-0.2.0-pyh9f0ad1d_0.tar.bz2#6006a6d08a3fa99268a2681c7fb55213 +https://conda.anaconda.org/conda-forge/noarch/boltons-23.1.1-pyhd8ed1ab_0.conda#56febe65315cc388a5d20adf2b39a74d +https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-1.1.0-h31becfc_1.conda#e41f5862ac746428407f3fd44d2ed01f +https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-python-1.1.0-py38hb83fbf6_1.conda#481316b31f7fba6fa88cb613f37fecf7 +https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2#576d629e47797577ab0f1b351297ef4a +https://conda.anaconda.org/conda-forge/noarch/certifi-2023.11.17-pyhd8ed1ab_0.conda#2011bcf45376341dd1d690263fdbc789 +https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.3.2-pyhd8ed1ab_0.conda#7f4a9e3fcff3f6356ae99244a014da6a +https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2#3faab06a954c2a04039983f2c4a50d99 +https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_0.conda#5cd86562580f274031ede6aa6aa24441 +https://conda.anaconda.org/conda-forge/linux-aarch64/debugpy-1.8.0-py38hb83fbf6_1.conda#bb4b478f78b779eaaaa5c9a1c4038d61 +https://conda.anaconda.org/conda-forge/noarch/decorator-5.1.1-pyhd8ed1ab_0.tar.bz2#43afe5ab04e35e17ba28649471dd7364 +https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2#961b3a227b437d82ad7054484cfa71b2 +https://conda.anaconda.org/conda-forge/noarch/distro-1.8.0-pyhd8ed1ab_0.conda#67999c5465064480fa8016d00ac768f6 +https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_0.tar.bz2#3cf04868fee0a029769bd41f4b2fbf2d +https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_0.conda#f6c211fee3c98229652b60a9a42ef363 +https://conda.anaconda.org/conda-forge/noarch/executing-2.0.1-pyhd8ed1ab_0.conda#e16be50e378d8a4533b989035b196ab8 +https://conda.anaconda.org/conda-forge/noarch/idna-3.6-pyhd8ed1ab_0.conda#1a76f09108576397c41c0b0c5bd84134 +https://conda.anaconda.org/conda-forge/noarch/ipython_genutils-0.2.0-py_1.tar.bz2#5071c982548b3a20caf70462f04f5287 +https://conda.anaconda.org/conda-forge/noarch/json5-0.9.14-pyhd8ed1ab_0.conda#dac1dabba2b5a9d1aee175c5fcc7b436 +https://conda.anaconda.org/conda-forge/linux-aarch64/jsonpointer-2.4-py38he3eb160_3.conda#69fd075f06a8a58f731ebbb262378df7 +https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.9-pyhd8ed1ab_0.conda#8370e0a9dc443f9b45a23fd30e7a6b3b +https://conda.anaconda.org/conda-forge/linux-aarch64/kiwisolver-1.4.5-py38h7e3a353_1.conda#7a0bdcd2a67eb35b54ca5d26073d598c +https://conda.anaconda.org/conda-forge/linux-aarch64/lcms2-2.16-h922389a_0.conda#ffdd8267a04c515e7ce69c727b051414 +https://conda.anaconda.org/conda-forge/linux-aarch64/libblas-3.9.0-20_linuxaarch64_openblas.conda#11590ed0fb5cebe7bbfa4bab8d8b07f8 +https://conda.anaconda.org/conda-forge/linux-aarch64/libcurl-8.5.0-h4e8248e_0.conda#fa0f5edc06ffc25a01eed005c6dc3d8c +https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-2.1.3-py38hea3b116_1.conda#cbb1053103239d7344600e53e28aa2b9 +https://conda.anaconda.org/conda-forge/linux-aarch64/menuinst-2.0.1-py38he3eb160_0.conda#1cda113300db70489451c83f8986f978 +https://conda.anaconda.org/conda-forge/noarch/mistune-3.0.2-pyhd8ed1ab_0.conda#5cbee699846772cc939bef23a0d524ed +https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 +https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.5.8-pyhd8ed1ab_0.conda#a4f0e4519bc50eee4f53f689be9607f7 +https://conda.anaconda.org/conda-forge/linux-aarch64/openjpeg-2.5.0-h0d9d63b_3.conda#123f5df3bc7f0e23c6950fddb97d1f43 +https://conda.anaconda.org/conda-forge/noarch/packaging-23.2-pyhd8ed1ab_0.conda#79002079284aa895f883c6b7f3f88fd6 +https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2#457c2c8c08e54905d6954e79cb5b5db9 +https://conda.anaconda.org/conda-forge/noarch/parso-0.8.3-pyhd8ed1ab_0.tar.bz2#17a565a0c3899244e938cdf417e7b094 +https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-py_1003.tar.bz2#415f0ebb6198cc2801c73438a9fb5761 +https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_1.conda#405678b942f2481cecdb3e010f4925d9 +https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.1.0-pyhd8ed1ab_0.conda#45a5065664da0d1dfa8f8cd2eaf05ab9 +https://conda.anaconda.org/conda-forge/noarch/pluggy-1.3.0-pyhd8ed1ab_0.conda#2390bd10bed1f3fdc7a537fb5a447d8d +https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.19.0-pyhd8ed1ab_0.conda#7baa10fa8073c371155cf451b71b848d +https://conda.anaconda.org/conda-forge/linux-aarch64/psutil-5.9.7-py38h9579f32_0.conda#400bdd3b46e6b4ba963c0a752a88b4b6 +https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd3deb0d_0.tar.bz2#359eeb6536da0e687af562ed265ec263 +https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.2-pyhd8ed1ab_0.tar.bz2#6784285c7e55cb7212efabc79e4c2883 +https://conda.anaconda.org/conda-forge/linux-aarch64/pycosat-0.6.6-py38h9579f32_0.conda#614fb7fd080f1d78ac521a7a526d15f6 +https://conda.anaconda.org/conda-forge/noarch/pycparser-2.21-pyhd8ed1ab_0.tar.bz2#076becd9e05608f8dc72757d5f3a91ff +https://conda.anaconda.org/conda-forge/noarch/pygments-2.17.2-pyhd8ed1ab_0.conda#140a7f159396547e9799aa98f9f0742e +https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.1-pyhd8ed1ab_0.conda#176f7d56f0cfe9008bdf1bccd7de02fb +https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2#2a7de29fb590ca14b5243c4c812c8025 +https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.19.0-pyhd8ed1ab_0.conda#e4dbdb3585c0266b4710467fe7b75cf4 +https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda#a61bf9ec79426938ff785eb69dbb1960 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2023.3-pyhd8ed1ab_0.conda#2590495f608a63625e165915fb4e2e34 +https://conda.anaconda.org/conda-forge/noarch/pytz-2023.3.post1-pyhd8ed1ab_0.conda#c93346b446cd08c169d843ae5fc0da97 +https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.1-py38h9579f32_1.conda#84a50ac70736cc75812b2a49b580415b +https://conda.anaconda.org/conda-forge/linux-aarch64/pyzmq-25.1.2-py38ha083373_0.conda#cfc022afe7d90a031feba5b199a48775 +https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2#912a71cc01012ee38e6b90ddd561e36f +https://conda.anaconda.org/conda-forge/linux-aarch64/rpds-py-0.15.2-py38hf532af8_0.conda#ad5867cfec23c308f027112764ed790a +https://conda.anaconda.org/conda-forge/linux-aarch64/ruamel.yaml.clib-0.2.7-py38h9579f32_2.conda#0f2220c05bc739b3d390a0bd12c9029a +https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.2-pyh41d4057_0.conda#ada5a17adcd10be4fc7e37e4166ba0e2 +https://conda.anaconda.org/conda-forge/noarch/setuptools-68.2.2-pyhd8ed1ab_0.conda#fc2166155db840c634a1291a5c35a709 +https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 +https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.0-pyhd8ed1ab_0.tar.bz2#dd6cbc539e74cb1f430efbd4575b9303 +https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda#3f144b2c34f8cb5a9abd9ed23a39c561 +https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 +https://conda.anaconda.org/conda-forge/linux-aarch64/tornado-6.3.3-py38hea3b116_1.conda#ad8257a473627cd17562cb7264f12dcb +https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.0-pyhd8ed1ab_0.conda#886f4a84ddb49b943b1697ac314e85b3 +https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.8.19.14-pyhd8ed1ab_0.conda#4df15c51a543e806d439490b862be1c6 +https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.9.0-pyha770c72_0.conda#a92a6440c3fe7052d63244f3aba2a4a7 +https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_0.tar.bz2#eb67e3cace64c66233e2d35949e20f92 +https://conda.anaconda.org/conda-forge/linux-aarch64/unicodedata2-15.1.0-py38h9579f32_0.conda#b9ed292773b0126e79d4cf5e6eeeab8a +https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_0.conda#0944dc65cb4a9b5b68522c3bb585d41c +https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.12-pyhd8ed1ab_0.conda#bf4a1d1a97ca27b0b65bacd9e238b484 +https://conda.anaconda.org/conda-forge/noarch/webcolors-1.13-pyhd8ed1ab_0.conda#166212fe82dad8735550030488a01d03 +https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_2.conda#daf5160ff9cde3a468556965329085b9 +https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.7.0-pyhd8ed1ab_0.conda#50ad31e07d706aae88b14a4ac9c73f23 +https://conda.anaconda.org/conda-forge/noarch/wheel-0.42.0-pyhd8ed1ab_0.conda#1cdea58981c5cbc17b51973bcaddcea7 +https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.9-pyhd8ed1ab_0.conda#82617d07b2f5f5a96296d3c19684b37a +https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4d6bc0b14e10f895fc6791a7d9b26a +https://conda.anaconda.org/conda-forge/noarch/anyio-4.2.0-pyhd8ed1ab_0.conda#81ce9f3d9697b534d95118bb86c8a07e +https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda#5f25798dcefd8252ce5f9dc494d5f571 +https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda#3d081de3a6ea9f894bbb585e8e3a4dcb +https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda#9669586875baeced8fc30c0826c3270e +https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.12.2-pyha770c72_0.conda#a362ff7d976217f8fa78c0f1c4f59717 +https://conda.anaconda.org/conda-forge/noarch/bleach-6.1.0-pyhd8ed1ab_0.conda#0ed9d7c0e9afa7c025807a9a8136ea3e +https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2#9b347a7ec10940d3f7941ff6c460b551 +https://conda.anaconda.org/conda-forge/linux-aarch64/cffi-1.16.0-py38h4ab679b_0.conda#601106b459f7b9300e8b7b90ab0e1f54 +https://conda.anaconda.org/conda-forge/noarch/comm-0.1.4-pyhd8ed1ab_0.conda#c8eaca39e2b6abae1fc96acc929ae939 +https://conda.anaconda.org/conda-forge/linux-aarch64/fonttools-4.47.0-py38h9579f32_0.conda#95e5753aa9291f5859ba3e489006bb24 +https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.0.1-pyha770c72_0.conda#746623a787e06191d80a2133e5daff17 +https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.1.1-pyhd8ed1ab_0.conda#3d5fa25cf42f3f32a12b2d874ace8574 +https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.1-pyhd8ed1ab_0.conda#81a3be0b2023e1ea8555781f0ad904a2 +https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.2-pyhd8ed1ab_1.tar.bz2#c8490ed5c70966d232fdd389d0dbed37 +https://conda.anaconda.org/conda-forge/noarch/jsonpatch-1.33-pyhd8ed1ab_0.conda#bfdb7c5c6ad1077c82a69a8642c87aff +https://conda.anaconda.org/conda-forge/linux-aarch64/jupyter_core-5.6.0-py38he3eb160_0.conda#cc54fc2bf08a423b6d4b31e028326c75 +https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_0.conda#3f0915b1fb2252ab73686a533c5f9d3f +https://conda.anaconda.org/conda-forge/linux-aarch64/libcblas-3.9.0-20_linuxaarch64_openblas.conda#b41e55ae2cb9d3518da2cbe3677b3b3b +https://conda.anaconda.org/conda-forge/linux-aarch64/liblapack-3.9.0-20_linuxaarch64_openblas.conda#e7412a592d9ee7c92026eb1189687271 +https://conda.anaconda.org/conda-forge/linux-aarch64/libmamba-1.5.6-hea3be6c_0.conda#feb0be90db1e014dde541cafa78efc15 +https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.6-pyhd8ed1ab_0.tar.bz2#b21613793fcc81d944c76c9f2864a7de +https://conda.anaconda.org/conda-forge/noarch/overrides-7.4.0-pyhd8ed1ab_0.conda#4625b7b01d7f4ac9c96300a5515acfaa +https://conda.anaconda.org/conda-forge/noarch/pexpect-4.8.0-pyh1a96a4e_2.tar.bz2#330448ce4403cc74990ac07c555942a1 +https://conda.anaconda.org/conda-forge/linux-aarch64/pillow-10.1.0-py38hf904494_0.conda#123d12e1f6093a2fad407005df6bb241 +https://conda.anaconda.org/conda-forge/noarch/pip-23.3.2-pyhd8ed1ab_0.conda#8591c748f98dcc02253003533bc2e4b1 +https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.42-pyha770c72_0.conda#0bf64bf10eee21f46ac83c161917fa86 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.8.2-pyhd8ed1ab_0.tar.bz2#dd999d1cc9f79e67dbb855c8924c7984 +https://conda.anaconda.org/conda-forge/noarch/referencing-0.32.0-pyhd8ed1ab_0.conda#a7b5a535cd614e384594530aee7e6061 +https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_0.tar.bz2#fed45fc5ea0813240707998abe49f520 +https://conda.anaconda.org/conda-forge/linux-aarch64/ruamel.yaml-0.18.5-py38h9579f32_0.conda#9ce349ff3a0c46af59796e9ea2014c4b +https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.0-pyh0d859eb_0.conda#e463f348b8b0eb62c9f7c6fbc780286c +https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.2.1-pyhd8ed1ab_0.tar.bz2#7234c9eefff659501cd2fe0d2ede4d48 +https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.1-pyhd8ed1ab_0.conda#03c97908b976498dcae97eb4e4f3149c +https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.9.0-hd8ed1ab_0.conda#c16524c1b7227dc80b36b4fa6f77cc86 +https://conda.anaconda.org/conda-forge/noarch/urllib3-2.1.0-pyhd8ed1ab_0.conda#f8ced8ee63830dec7ecc1be048d1470a +https://conda.anaconda.org/conda-forge/linux-aarch64/argon2-cffi-bindings-21.2.0-py38h9579f32_4.conda#3460f222597376592b6e1ac5eac0bdce +https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_0.conda#b77d8c2313158e6e461ca0efb1c2c508 +https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_0.tar.bz2#642d35437078749ef23a5dca2c9bb1f3 +https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.1.1-pyhd8ed1ab_0.conda#d04bd1b5bed9177dd7c3cef15e2b6710 +https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-7.0.1-hd8ed1ab_0.conda#4a2f43a20fa404b998859c6a470ba316 +https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2023.11.2-pyhd8ed1ab_0.conda#73884ca36d6d96cbce498cde99fab40f +https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.1-pyhd8ed1ab_0.conda#919e6d570f8b3839f3a1ed99b25088af +https://conda.anaconda.org/conda-forge/linux-aarch64/libmambapy-1.5.6-py38h513f8d8_0.conda#58b6a04a2bebe603ab1fdeaac7c1362b +https://conda.anaconda.org/conda-forge/linux-aarch64/numpy-1.24.4-py38he3f4005_0.conda#da4a2794009cc9a747ed1e5d16bbdf77 +https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.42-hd8ed1ab_0.conda#85a2189ecd2fcdd86e92b2d4ea8fe461 +https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda#a30144e4156cdbb236f99ebb49828f8b +https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda#e7df0fdd404616638df5ece6e69ba7af +https://conda.anaconda.org/conda-forge/linux-aarch64/zstandard-0.22.0-py38hc2e9a1f_0.conda#93dac68ac541ed7cc1ba83ed34691ebf +https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-23.1.0-pyhd8ed1ab_0.conda#3afef1f55a1366b4d3b6a0d92e2235e4 +https://conda.anaconda.org/conda-forge/noarch/conda-package-streaming-0.9.0-pyhd8ed1ab_0.conda#38253361efb303deead3eab39ae9269b +https://conda.anaconda.org/conda-forge/linux-aarch64/contourpy-1.1.1-py38hb4b5b6f_1.conda#6e4c4fe499bfbd85d40fa5de0f82a837 +https://conda.anaconda.org/conda-forge/noarch/ipython-8.12.2-pyh41d4057_0.conda#acebfd89278ecac2a67b60b657e00d5c +https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_0.tar.bz2#4cb68948e0b8429534380243d063a27a +https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.20.0-pyhd8ed1ab_0.conda#1116d79def5268414fb0917520b2bbf1 +https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.0-pyhd8ed1ab_0.conda#6bd3f1069cdebb44c7ae9efb900e312d +https://conda.anaconda.org/conda-forge/linux-aarch64/pandas-2.0.3-py38h958bb2c_1.conda#1e30c2de3a3ce99ab1a4ab068a7551b9 +https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.0-pyhd8ed1ab_0.conda#134b2b57b7865d2316a7cce1915a51ed +https://conda.anaconda.org/conda-forge/noarch/conda-package-handling-2.2.0-pyh38be061_0.conda#8a3ae7f6318376aa08ea753367bb7dd6 +https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.28.0-pyhd33586a_0.conda#726e1192b05b38c8c008fd67bc237969 +https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.1-pyhd8ed1ab_0.conda#2605fae5ee27100e5f10037baebf4d41 +https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.20.0-pyhd8ed1ab_0.conda#a168c5f84010711f6d4ae650bc22b480 +https://conda.anaconda.org/conda-forge/linux-aarch64/matplotlib-base-3.7.3-py38h7709db9_0.conda#e4b21e67e5e4f7532501a8f83ef6c107 +https://conda.anaconda.org/conda-forge/noarch/nbformat-5.9.2-pyhd8ed1ab_0.conda#61ba076de6530d9301a0053b02f093d2 +https://conda.anaconda.org/conda-forge/linux-aarch64/scipy-1.10.1-py38he3f4005_3.conda#ea7219314f2ffcbaefeec1e42a15122f +https://conda.anaconda.org/conda-forge/noarch/ipympl-0.9.3-pyhd8ed1ab_0.conda#da113e1ecd782afd5ed2f7b5187aaea8 +https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.9.0-pyhd8ed1ab_0.conda#00ba25993f0dba38cf72a7224e33289f +https://conda.anaconda.org/conda-forge/noarch/nbclient-0.8.0-pyhd8ed1ab_0.conda#e78da91cf428faaf05701ce8cc8f2f9b +https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.13.1-pyhd8ed1ab_0.conda#165cac4486f9e8542f0b8de32822f328 +https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.12.1-pyhd8ed1ab_0.conda#e9781be1e6c93b5df2c180a9f9242420 +https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.2.1-pyhd8ed1ab_0.conda#d1a5efc65bfabc3bfebf4d3a204da897 +https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.25.2-pyhd8ed1ab_0.conda#f45557d5551b54dc2a74133a310bc1ba +https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.3-pyhd8ed1ab_0.conda#67e0fe74c156267d9159e9133df7fd37 +https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.0.7-pyhd8ed1ab_0.conda#80318d83f33b3bf4e57b8533b7a6691d +https://conda.anaconda.org/conda-forge/linux-aarch64/conda-23.11.0-py38he3eb160_1.conda#d9d7c83ff042fab98220a5cc7edac213 +https://conda.anaconda.org/conda-forge/noarch/conda-libmamba-solver-23.12.0-pyhd8ed1ab_0.conda#e877d5150e73a0844ea2939be110c3b1 diff --git a/env_installer/jlab_server.yaml b/env_installer/jlab_server.yaml index 0f64470f..e04fa306 100644 --- a/env_installer/jlab_server.yaml +++ b/env_installer/jlab_server.yaml @@ -14,6 +14,7 @@ dependencies: - scipy platforms: - linux-64 + - linux-aarch64 - osx-64 - osx-arm64 - win-64 diff --git a/package.json b/package.json index f77d4e63..16e23e13 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "update_conda_lock": "cd env_installer && rimraf *.lock && conda-lock --kind explicit -f jlab_server.yaml && cd -", "clean_env_installer": "rimraf ./env_installer/jlab_server.tar.gz && rimraf ./env_installer/jlab_server", "create_env_installer:linux": "yarn clean_env_installer && conda-lock install --prefix ./env_installer/jlab_server ./env_installer/conda-linux-64.lock && conda pack -p ./env_installer/jlab_server -o ./env_installer/jlab_server.tar.gz", + "create_env_installer:linux-aarch64": "yarn clean_env_installer && conda-lock install --prefix ./env_installer/jlab_server ./env_installer/conda-linux-aarch64.lock && conda pack -p ./env_installer/jlab_server -o ./env_installer/jlab_server.tar.gz", "create_env_installer:osx-64": "yarn clean_env_installer && conda-lock install --prefix ./env_installer/jlab_server ./env_installer/conda-osx-64.lock && conda pack -p ./env_installer/jlab_server -o ./env_installer/jlab_server.tar.gz", "create_env_installer:osx-arm64": "yarn clean_env_installer && conda-lock install --no-validate-platform --prefix ./env_installer/jlab_server ./env_installer/conda-osx-arm64.lock && conda pack -p ./env_installer/jlab_server -o ./env_installer/jlab_server.tar.gz", "create_env_installer:win": "yarn clean_env_installer && conda-lock install --prefix ./env_installer/jlab_server ./env_installer/conda-win-64.lock && conda pack -p ./env_installer/jlab_server -o ./env_installer/jlab_server.tar.gz", From d82601ac04e19fda49e9f38d46a9ab271ca47286 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Tue, 26 Dec 2023 20:34:43 -0800 Subject: [PATCH 03/62] fix popup height calculation --- src/main/pythonenvselectpopup/pythonenvselectpopup.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/pythonenvselectpopup/pythonenvselectpopup.ts b/src/main/pythonenvselectpopup/pythonenvselectpopup.ts index f7e52a2a..5121070c 100644 --- a/src/main/pythonenvselectpopup/pythonenvselectpopup.ts +++ b/src/main/pythonenvselectpopup/pythonenvselectpopup.ts @@ -297,10 +297,11 @@ export class PythonEnvironmentSelectPopup { getScrollHeight(): number { const envCount = this._envs.length; return ( + 30 + // title 40 + // path input (envCount > 0 ? envCount * 40 + 14 : 0) + // env list - 12 - ); // padding + 17 // padding + ); } private _view: ThemedView; From 69aa8dfe78eed4358f3df5277435d2a33a8a0374 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Wed, 27 Dec 2023 10:11:11 -0800 Subject: [PATCH 04/62] store additional env install info --- src/main/cli.ts | 7 ++++++- src/main/utils.ts | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/cli.ts b/src/main/cli.ts index 85a21f89..096a536e 100644 --- a/src/main/cli.ts +++ b/src/main/cli.ts @@ -19,6 +19,7 @@ import { appData } from './config/appdata'; import { IEnvironmentType, IPythonEnvironment } from './tokens'; import { SettingType, userSettings } from './config/settings'; import { Registry } from './registry'; +import { app } from 'electron'; export function parseCLIArgs(argv: string[]) { return yargs(argv) @@ -346,7 +347,11 @@ export async function createPythonEnvironment( } } - markEnvironmentAsJupyterInstalled(envPath); + markEnvironmentAsJupyterInstalled(envPath, { + type: isConda ? 'conda' : 'venv', + source: 'registry', + appVersion: app.getVersion() + }); addUserSetEnvironment(envPath, isConda); } diff --git a/src/main/utils.ts b/src/main/utils.ts index 8596f70f..6fa07f32 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -325,7 +325,11 @@ export async function installBundledEnvironment( return; } - markEnvironmentAsJupyterInstalled(installPath); + markEnvironmentAsJupyterInstalled(installPath, { + type: 'conda', + source: 'bundled-installer', + appVersion: app.getVersion() + }); let unpackCommand = isWin ? `${installPath}\\Scripts\\activate.bat && conda-unpack` From 44b6aa797230af533e8d51859316e051624156ff Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Wed, 27 Dec 2023 19:08:22 -0800 Subject: [PATCH 05/62] env delete implementation --- src/main/app.ts | 39 ++++- src/main/eventtypes.ts | 8 +- src/main/pythonenvdialog/preload.ts | 18 ++ src/main/pythonenvdialog/pythonenvdialog.ts | 185 +++++++++++++------- src/main/utils.ts | 40 +++++ 5 files changed, 213 insertions(+), 77 deletions(-) diff --git a/src/main/app.ts b/src/main/app.ts index d5c851d9..00a4de88 100644 --- a/src/main/app.ts +++ b/src/main/app.ts @@ -406,14 +406,12 @@ export class JupyterApplication implements IApplication, IDisposable { } async showManagePythonEnvsDialog() { - const dialog = new ManagePythonEnvironmentDialog( - { - envs: await this._registry.getEnvironmentList(false), - isDarkTheme: this._isDarkTheme, - defaultPythonPath: userSettings.getValue(SettingType.pythonPath) - }, - this._registry - ); + const dialog = new ManagePythonEnvironmentDialog({ + envs: await this._registry.getEnvironmentList(false), + isDarkTheme: this._isDarkTheme, + defaultPythonPath: userSettings.getValue(SettingType.pythonPath), + app: this + }); dialog.load(); } @@ -448,6 +446,17 @@ export class JupyterApplication implements IApplication, IDisposable { } } + get registry(): IRegistry { + return this._registry; + } + + async updateRegistry() { + const registry = new Registry(); + await registry.ready; + this._registry = registry; + appData.save(); + } + dispose(): Promise { if (this._disposePromise) { return this._disposePromise; @@ -771,6 +780,20 @@ export class JupyterApplication implements IApplication, IDisposable { } ); + this._evm.registerSyncEventHandler( + EventTypeMain.UpdateRegistry, + (event, cacheOK) => { + return this.updateRegistry(); + } + ); + + this._evm.registerSyncEventHandler( + EventTypeMain.GetPythonEnvironmentList, + (event, cacheOK) => { + return this._registry.getEnvironmentList(cacheOK); + } + ); + this._evm.registerSyncEventHandler( EventTypeMain.ValidateRemoteServerUrl, (event, url) => { diff --git a/src/main/eventtypes.ts b/src/main/eventtypes.ts index 7fab6185..ffd1ecb6 100644 --- a/src/main/eventtypes.ts +++ b/src/main/eventtypes.ts @@ -63,7 +63,10 @@ export enum EventTypeMain { CreateNewPythonEnvironment = 'create-new-python-environment', ShowManagePythonEnvironmentsDialog = 'show-manage-python-environments-dialog', SelectPythonEnvInstallDirectory = 'select-python-environment-install-directory', - ShowPythonEnvironmentContextMenu = 'show-python-environment-context-menu' + ShowPythonEnvironmentContextMenu = 'show-python-environment-context-menu', + DeletePythonEnvironment = 'delete-python-environment', + GetPythonEnvironmentList = 'get-python-environment-list', + UpdateRegistry = 'update-registry' } // events sent to Renderer process @@ -83,5 +86,6 @@ export enum EventTypeRenderer { SetNotificationMessage = 'set-notification-message', DisableLocalServerActions = 'disable-local-server-actions', SetDefaultWorkingDirectoryResult = 'set-default-working-directory-result', - ResetPythonEnvSelectPopup = 'reset-python-env-select-popup' + ResetPythonEnvSelectPopup = 'reset-python-env-select-popup', + SetPythonEnvironmentList = 'set-python-environment-list' } diff --git a/src/main/pythonenvdialog/preload.ts b/src/main/pythonenvdialog/preload.ts index a9476cb1..4cb472a8 100644 --- a/src/main/pythonenvdialog/preload.ts +++ b/src/main/pythonenvdialog/preload.ts @@ -1,4 +1,5 @@ import { EventTypeMain, EventTypeRenderer } from '../eventtypes'; +import { IPythonEnvironment } from '../tokens'; const { contextBridge, ipcRenderer } = require('electron'); @@ -8,10 +9,12 @@ type InstallBundledPythonEnvStatusListener = ( ) => void; type CustomPythonPathSelectedListener = (path: string) => void; type WorkingDirectorySelectedListener = (path: string) => void; +type SetPythonEnvironmentListListener = (envs: IPythonEnvironment[]) => void; let onInstallBundledPythonEnvStatusListener: InstallBundledPythonEnvStatusListener; let onCustomPythonPathSelectedListener: CustomPythonPathSelectedListener; let onWorkingDirectorySelectedListener: WorkingDirectorySelectedListener; +let onSetPythonEnvironmentListListener: SetPythonEnvironmentListListener; contextBridge.exposeInMainWorld('electronAPI', { getAppConfig: () => { @@ -28,6 +31,12 @@ contextBridge.exposeInMainWorld('electronAPI', { getNextPythonEnvironmentName: () => { return ipcRenderer.invoke(EventTypeMain.GetNextPythonEnvironmentName); }, + updateRegistry: () => { + return ipcRenderer.invoke(EventTypeMain.UpdateRegistry); + }, + getPythonEnvironmentList: (cacheOK: boolean) => { + return ipcRenderer.invoke(EventTypeMain.GetPythonEnvironmentList, cacheOK); + }, createNewPythonEnvironment: ( envPath: string, envType: string, @@ -83,6 +92,9 @@ contextBridge.exposeInMainWorld('electronAPI', { onWorkingDirectorySelected: (callback: WorkingDirectorySelectedListener) => { onWorkingDirectorySelectedListener = callback; }, + onSetPythonEnvironmentList: (callback: SetPythonEnvironmentListListener) => { + onSetPythonEnvironmentListListener = callback; + }, setDefaultWorkingDirectory: (path: string) => { ipcRenderer.send(EventTypeMain.SetDefaultWorkingDirectory, path); }, @@ -157,4 +169,10 @@ ipcRenderer.on(EventTypeRenderer.CustomPythonPathSelected, (event, path) => { } }); +ipcRenderer.on(EventTypeRenderer.SetPythonEnvironmentList, (event, envs) => { + if (onSetPythonEnvironmentListListener) { + onSetPythonEnvironmentListListener(envs); + } +}); + export {}; diff --git a/src/main/pythonenvdialog/pythonenvdialog.ts b/src/main/pythonenvdialog/pythonenvdialog.ts index a43267cf..2142258f 100644 --- a/src/main/pythonenvdialog/pythonenvdialog.ts +++ b/src/main/pythonenvdialog/pythonenvdialog.ts @@ -12,9 +12,9 @@ import { import * as path from 'path'; import * as fs from 'fs'; import { ThemedWindow } from '../dialog/themedwindow'; -import { IRegistry } from '../registry'; -import { IEnvironmentType, IPythonEnvironment } from '../tokens'; +import { IPythonEnvironment } from '../tokens'; import { + deletePythonEnvironment, envPathForPythonPath, getBundledPythonPath, getCondaPath, @@ -24,14 +24,12 @@ import { versionWithoutSuffix } from '../utils'; import { EventManager } from '../eventmanager'; -import { EventTypeMain } from '../eventtypes'; +import { EventTypeMain, EventTypeRenderer } from '../eventtypes'; +import { JupyterApplication } from '../app'; export class ManagePythonEnvironmentDialog { - constructor( - options: ManagePythonEnvironmentDialog.IOptions, - registry: IRegistry - ) { - this._registry = registry; + constructor(options: ManagePythonEnvironmentDialog.IOptions) { + this._app = options.app; this._window = new ThemedWindow({ isDarkTheme: options.isDarkTheme, title: 'Manage Python environments', @@ -61,7 +59,9 @@ export class ManagePythonEnvironmentDialog { if (bundledEnvInstallationExists) { try { - const bundledEnv = registry.getEnvironmentByPath(bundledPythonPath); + const bundledEnv = this._app.registry.getEnvironmentByPath( + bundledPythonPath + ); const jlabVersion = bundledEnv.versions['jupyterlab']; const appVersion = app.getVersion(); @@ -75,18 +75,6 @@ export class ManagePythonEnvironmentDialog { } } - const condaEnvs: IPythonEnvironment[] = options.envs.filter( - env => - env.type === IEnvironmentType.CondaEnv || - env.type === IEnvironmentType.CondaRoot - ); - const venvEnvs: IPythonEnvironment[] = options.envs.filter( - env => env.type === IEnvironmentType.VirtualEnv - ); - const globalEnvs: IPythonEnvironment[] = options.envs.filter( - env => env.type === IEnvironmentType.Path - ); - const infoIconSrc = fs.readFileSync( path.join(__dirname, '../../../app-assets/info-icon.svg') ); @@ -113,7 +101,7 @@ export class ManagePythonEnvironmentDialog { { label: 'Copy environment info', click: () => { - const env = this._registry.getEnvironmentByPath(pythonPath); + const env = this._app.registry.getEnvironmentByPath(pythonPath); if (env) { clipboard.writeText( JSON.stringify({ @@ -132,10 +120,17 @@ export class ManagePythonEnvironmentDialog { { type: 'separator', visible: installedByApp }, { label: 'Delete', - visible: installedByApp - // click: () => { - // this._app.showSettingsDialog(); - // } + visible: installedByApp, + click: async () => { + try { + await deletePythonEnvironment(envPath); + await this._app.updateRegistry(); + const envs = await this._app.registry.getEnvironmentList(true); + this.setPythonEnvironmentList(envs); + } catch (error) { + // + } + } } ]; @@ -146,25 +141,6 @@ export class ManagePythonEnvironmentDialog { } ); - const envTypeTemplate = ` - <% - function getEnvTooltip(env) { - const packages = []; - for (const name in env.versions) { - packages.push(name + ': ' + env.versions[name]); - } - return env.name + '\\n' + env.path + '\\n' + packages.join(', '); - } - %> -
<%- envType %> (<%- envs.length %>)
- <% envs.forEach(env => { %> - -
<%- env.path %>
-
<%- env.name %>
${menuIconSrc}
-
- <% }); %> - `; - const template = `
@@ -401,7 +419,9 @@ export class ManagePythonEnvironmentDialog { Name
${infoIconSrc}
- + +
${checkIconSrc}
${xMarkIconSrc}
+
@@ -496,7 +516,9 @@ export class ManagePythonEnvironmentDialog {
- + +
${checkIconSrc}
${xMarkIconSrc}
+
Select path @@ -510,7 +532,9 @@ export class ManagePythonEnvironmentDialog {
- + +
${checkIconSrc}
${xMarkIconSrc}
+
Select path @@ -524,10 +548,12 @@ export class ManagePythonEnvironmentDialog {
- + +
${checkIconSrc}
${xMarkIconSrc}
+
- Select path + Select path
@@ -567,12 +593,20 @@ export class ManagePythonEnvironmentDialog { const envListContainer = document.getElementById('env-list'); const pythonEnvInstallDirectoryInput = document.getElementById('python-env-install-directory'); + const condaPathInput = document.getElementById('conda-path'); + const systemPythonPathInput = document.getElementById('system-python-path'); let defaultPythonEnvChanged = false; let envs = <%- JSON.stringify(envs) %>; const pythonEnvInstallPath = "<%- pythonEnvInstallPath %>"; const pathSeparator = '${path.sep}'; + const debounceWait = 200; + let nameInputValid = false; + let nameInputValidationTimer = -1; + let envsDirInputValidationTimer = -1; + let condaPathInputValidationTimer = -1; + let systemPythonPathInputValidationTimer = -1; function handleEnvMenuClick(el) { const menuItem = el.closest('jp-menu-item'); @@ -597,9 +631,24 @@ export class ManagePythonEnvironmentDialog { } function handleSelectPythonEnvInstallDirectory() { - window.electronAPI.selectPythonEnvInstallDirectory().then(selected => { + window.electronAPI.selectDirectoryPath().then(selected => { pythonEnvInstallDirectoryInput.value = selected; - }) + handlePythonEnvsDirInputChange(); + }); + } + + function handleSelectCondaPath(el) { + window.electronAPI.selectFilePath().then(selected => { + condaPathInput.value = selected; + handleCondaPathInputChange(); + }); + } + + function handleSelectSystemPythonPath(el) { + window.electronAPI.selectFilePath().then(selected => { + systemPythonPathInput.value = selected; + handleSystemPythonPathInputChange(); + }); } function showBundledEnvWarning(type) { @@ -659,7 +708,7 @@ export class ManagePythonEnvironmentDialog { } function generateEnvTypeList(name, envs) { - let html = '
' + name + '(' + envs.length + ')
'; + let html = '
' + name + ' (' + envs.length + ')
'; for (const env of envs) { html += \` @@ -725,7 +774,7 @@ export class ManagePythonEnvironmentDialog { createEnvOutputRow.style.display = "none"; toggleInstallOutputButton.style.display = 'none'; clearCreateFormButton.style.display = 'none'; - handleNewEnvNameChange(); + handleNewEnvNameInputChange(); showProgress(''); createButton.disabled = false; } @@ -903,16 +952,94 @@ export class ManagePythonEnvironmentDialog { return \`\$\{pythonEnvInstallPath + pathSeparator + newEnvNameInput.value\}\`; } - function handleNewEnvNameChange() { + function handleNewEnvNameInputChange() { envInstallPathLabel.innerText = \`Installation path: "\$\{getEnvInstallPath()\}"\`; updateCreateCommandPreview(); + validateNameInput(); + } + + function handlePythonEnvsDirInputChange() { + validateAndUpdatePythonEnvsDir(); + } + + function handleCondaPathInputChange() { + validateAndUpdateCondaPath(); + } + + function handleSystemPythonPathInputChange() { + validateAndUpdateSystemPythonPath(); + } + + function showInputValidStatus(input, valid, message) { + const validIcon = input.getElementsByClassName('valid-icon')[0]; + const invalidIcon = input.getElementsByClassName('invalid-icon')[0]; + validIcon.style.display = valid ? 'block' : 'none'; + invalidIcon.style.display = valid ? 'none' : 'block'; + invalidIcon.title = message || ''; + } + function clearInputValidStatus(input) { + const validIcon = input.getElementsByClassName('valid-icon')[0]; + const invalidIcon = input.getElementsByClassName('invalid-icon')[0]; + validIcon.style.display = 'none'; + invalidIcon.style.display = 'none'; + } + + function validateNameInput() { + clearTimeout(nameInputValidationTimer); + nameInputValidationTimer = setTimeout(async () => { + clearInputValidStatus(newEnvNameInput); + const response = await window.electronAPI.validateNewPythonEnvironmentName(newEnvNameInput.value); + showInputValidStatus(newEnvNameInput, response.valid, response.message); + nameInputValid = response.valid; + updateCreateButtonState(); + }, debounceWait); + } + + function validateAndUpdatePythonEnvsDir() { + clearTimeout(envsDirInputValidationTimer); + envsDirInputValidationTimer = setTimeout(async () => { + clearInputValidStatus(pythonEnvInstallDirectoryInput); + const response = await window.electronAPI.validatePythonEnvironmentInstallDirectory(pythonEnvInstallDirectoryInput.value); + showInputValidStatus(pythonEnvInstallDirectoryInput, response.valid, response.message); + if (response.valid) { + window.electronAPI.setPythonEnvironmentInstallDirectory(pythonEnvInstallDirectoryInput.value); + } + }, debounceWait); + } + + function validateAndUpdateCondaPath() { + clearTimeout(condaPathInputValidationTimer); + condaPathInputValidationTimer = setTimeout(async () => { + clearInputValidStatus(condaPathInput); + const response = await window.electronAPI.validateCondaPath(condaPathInput.value); + showInputValidStatus(condaPathInput, response.valid, response.message); + if (response.valid) { + window.electronAPI.setCondaPath(condaPathInput.value); + } + }, debounceWait); + } + + function validateAndUpdateSystemPythonPath() { + clearTimeout(systemPythonPathInputValidationTimer); + systemPythonPathInputValidationTimer = setTimeout(async () => { + clearInputValidStatus(systemPythonPathInput); + const response = await window.electronAPI.validateSystemPythonPath(systemPythonPathInput.value); + showInputValidStatus(systemPythonPathInput, response.valid, response.message); + if (response.valid) { + window.electronAPI.setSystemPythonPath(systemPythonPathInput.value); + } + }, debounceWait); + } + + function updateCreateButtonState() { + createButton.disabled = !nameInputValid; } document.addEventListener("DOMContentLoaded", () => { updatePythonEnvironmentList(); handleDefaultPythonEnvTypeChange(); handleNewEnvCreateMethodChange(); - handleNewEnvNameChange(); + handleNewEnvNameInputChange(); <%- !bundledEnvInstallationExists ? 'showBundledEnvWarning("does-not-exist");' : '' %> <%- (bundledEnvInstallationExists && !bundledEnvInstallationLatest) ? 'showBundledEnvWarning("not-latest");' : '' %> ${ @@ -922,6 +1049,11 @@ export class ManagePythonEnvironmentDialog { ` : '' } + setTimeout(() => { + handlePythonEnvsDirInputChange(); + handleCondaPathInputChange(); + handleSystemPythonPathInputChange(); + }, 1000); }); `; @@ -944,7 +1076,7 @@ export class ManagePythonEnvironmentDialog { load() { this._window.loadDialogContent(this._pageBody); - this._window.window.on('close', () => { + this._window.window.on('closed', () => { this._evm.dispose(); }); } diff --git a/src/main/sessionwindow/sessionwindow.ts b/src/main/sessionwindow/sessionwindow.ts index 8cc6436b..bdb51a34 100644 --- a/src/main/sessionwindow/sessionwindow.ts +++ b/src/main/sessionwindow/sessionwindow.ts @@ -48,7 +48,7 @@ import { SessionConfig } from '../config/sessionconfig'; import { ISignal, Signal } from '@lumino/signaling'; import { EventTypeMain } from '../eventtypes'; import { EventManager } from '../eventmanager'; -import { runCommandInEnvironment } from '../cli'; +import { runCommandInEnvironment } from '../env'; export enum ContentViewType { Welcome = 'welcome', From b6dec5d713280a1972701a3ca7d5299d1b3836fe Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Sat, 30 Dec 2023 19:24:18 -0800 Subject: [PATCH 11/62] add env list description, update styles --- src/main/pythonenvdialog/pythonenvdialog.ts | 139 +++++++++++--------- 1 file changed, 74 insertions(+), 65 deletions(-) diff --git a/src/main/pythonenvdialog/pythonenvdialog.ts b/src/main/pythonenvdialog/pythonenvdialog.ts index a6e88cca..9a4f8eb0 100644 --- a/src/main/pythonenvdialog/pythonenvdialog.ts +++ b/src/main/pythonenvdialog/pythonenvdialog.ts @@ -165,6 +165,7 @@ export class ManagePythonEnvironmentDialog { height: 100%; } #content-area { + background: var(--neutral-layer-4); display: flex; flex-direction: row; column-gap: 20px; @@ -378,6 +379,10 @@ export class ManagePythonEnvironmentDialog { jp-text-field .invalid-icon svg path { fill: var(--error-fill-hover); } + .env-list-description-row { + padding: 5px; + color: var(--neutral-foreground-hint); + }
@@ -386,17 +391,21 @@ export class ManagePythonEnvironmentDialog { Settings - Available Python environments -
-
- Add existing - Create new +
+
+
+ Python paths for compatible environments discovered on your system are listed below. You can add other environments by selecting a Python executable path on your system, or create new environments. 'jupyterlab' Python package needs to be installed in an environment to be compatible with JupyterLab Desktop. +
+
+ Add existing + Create new +
-
-
- - +
+ + +
@@ -484,76 +493,76 @@ export class ManagePythonEnvironmentDialog { - Python environment settings - -
-
- Default Python path for JupyterLab Server
${infoIconSrc}
-
-
-
-
InstallUpdate
- - <%= !bundledEnvInstallationExists ? 'disabled' : '' %> onchange="handleDefaultPythonEnvTypeChange(this);">Use bundled Python environment installation - onchange="handleDefaultPythonEnvTypeChange(this);">Use custom Python environment - - -
-
- -
-
- Select path +
+
+
+ Default Python path for JupyterLab Server
${infoIconSrc}
+
+
+
+
InstallUpdate
+ + <%= !bundledEnvInstallationExists ? 'disabled' : '' %> onchange="handleDefaultPythonEnvTypeChange(this);">Use bundled Python environment installation + onchange="handleDefaultPythonEnvTypeChange(this);">Use custom Python environment + + +
+
+ +
+
+ Select path +
-
-
-
- New Python environment install directory
${infoIconSrc}
-
-
-
- -
${checkIconSrc}
${xMarkIconSrc}
-
+
+
+ New Python environment install directory
${infoIconSrc}
-
- Select path +
+
+ +
${checkIconSrc}
${xMarkIconSrc}
+
+
+
+ Select path +
-
-
-
- conda path
${infoIconSrc}
-
-
-
- -
${checkIconSrc}
${xMarkIconSrc}
-
+
+
+ conda path
${infoIconSrc}
-
- Select path +
+
+ +
${checkIconSrc}
${xMarkIconSrc}
+
+
+
+ Select path +
-
-
-
- Python path to use when creating venv environments
${infoIconSrc}
-
-
-
- -
${checkIconSrc}
${xMarkIconSrc}
-
+
+
+ Python path to use when creating venv environments
${infoIconSrc}
-
- Select path +
+
+ +
${checkIconSrc}
${xMarkIconSrc}
+
+
+
+ Select path +
From 29c1f04adb9ff4c1c9a8a187a38e6b1a1130677d Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Sat, 30 Dec 2023 21:33:27 -0800 Subject: [PATCH 12/62] handle env delete and add existing errors --- src/assets/xmark-circle.svg | 1 + src/assets/xmark.svg | 2 +- src/main/eventtypes.ts | 3 +- src/main/pythonenvdialog/preload.ts | 19 ++++ src/main/pythonenvdialog/pythonenvdialog.ts | 109 +++++++++++++++++--- src/main/registry.ts | 5 +- 6 files changed, 118 insertions(+), 21 deletions(-) create mode 100644 src/assets/xmark-circle.svg diff --git a/src/assets/xmark-circle.svg b/src/assets/xmark-circle.svg new file mode 100644 index 00000000..e108b5d8 --- /dev/null +++ b/src/assets/xmark-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/xmark.svg b/src/assets/xmark.svg index e108b5d8..38549308 100644 --- a/src/assets/xmark.svg +++ b/src/assets/xmark.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/main/eventtypes.ts b/src/main/eventtypes.ts index d4dfef98..e40b8ac1 100644 --- a/src/main/eventtypes.ts +++ b/src/main/eventtypes.ts @@ -97,5 +97,6 @@ export enum EventTypeRenderer { DisableLocalServerActions = 'disable-local-server-actions', SetDefaultWorkingDirectoryResult = 'set-default-working-directory-result', ResetPythonEnvSelectPopup = 'reset-python-env-select-popup', - SetPythonEnvironmentList = 'set-python-environment-list' + SetPythonEnvironmentList = 'set-python-environment-list', + SetEnvironmentListUpdateStatus = 'set-environment-list-update-status' } diff --git a/src/main/pythonenvdialog/preload.ts b/src/main/pythonenvdialog/preload.ts index 2884d65e..818c72bc 100644 --- a/src/main/pythonenvdialog/preload.ts +++ b/src/main/pythonenvdialog/preload.ts @@ -10,11 +10,16 @@ type InstallBundledPythonEnvStatusListener = ( type CustomPythonPathSelectedListener = (path: string) => void; type WorkingDirectorySelectedListener = (path: string) => void; type SetPythonEnvironmentListListener = (envs: IPythonEnvironment[]) => void; +type EnvironmentListUpdateStatusListener = ( + status: string, + message: string +) => void; let onInstallBundledPythonEnvStatusListener: InstallBundledPythonEnvStatusListener; let onCustomPythonPathSelectedListener: CustomPythonPathSelectedListener; let onWorkingDirectorySelectedListener: WorkingDirectorySelectedListener; let onSetPythonEnvironmentListListener: SetPythonEnvironmentListListener; +let onEnvironmentListUpdateStatusListener: EnvironmentListUpdateStatusListener; contextBridge.exposeInMainWorld('electronAPI', { getAppConfig: () => { @@ -101,6 +106,11 @@ contextBridge.exposeInMainWorld('electronAPI', { onSetPythonEnvironmentList: (callback: SetPythonEnvironmentListListener) => { onSetPythonEnvironmentListListener = callback; }, + onEnvironmentListUpdateStatus: ( + callback: EnvironmentListUpdateStatusListener + ) => { + onEnvironmentListUpdateStatusListener = callback; + }, setDefaultWorkingDirectory: (path: string) => { ipcRenderer.send(EventTypeMain.SetDefaultWorkingDirectory, path); }, @@ -226,4 +236,13 @@ ipcRenderer.on(EventTypeRenderer.SetPythonEnvironmentList, (event, envs) => { } }); +ipcRenderer.on( + EventTypeRenderer.SetEnvironmentListUpdateStatus, + (event, status, message) => { + if (onEnvironmentListUpdateStatusListener) { + onEnvironmentListUpdateStatusListener(status, message); + } + } +); + export {}; diff --git a/src/main/pythonenvdialog/pythonenvdialog.ts b/src/main/pythonenvdialog/pythonenvdialog.ts index 9a4f8eb0..62d9141f 100644 --- a/src/main/pythonenvdialog/pythonenvdialog.ts +++ b/src/main/pythonenvdialog/pythonenvdialog.ts @@ -90,6 +90,9 @@ export class ManagePythonEnvironmentDialog { const xMarkIconSrc = fs.readFileSync( path.join(__dirname, '../../../app-assets/xmark.svg') ); + const xMarkCircleIconSrc = fs.readFileSync( + path.join(__dirname, '../../../app-assets/xmark-circle.svg') + ); const pythonEnvName = getNextPythonEnvName(); const pythonEnvInstallPath = getPythonEnvsDirectory(); @@ -135,13 +138,29 @@ export class ManagePythonEnvironmentDialog { label: 'Delete', visible: deletable, click: async () => { + this._window.window.webContents.send( + EventTypeRenderer.SetEnvironmentListUpdateStatus, + 'ENV-DELETE-RUNNING' + ); try { await deletePythonEnvironment(envPath); + this._window.window.webContents.send( + EventTypeRenderer.SetEnvironmentListUpdateStatus, + 'ENV-DELETE-RUNNING' + ); await this._app.updateRegistry(); + this._window.window.webContents.send( + EventTypeRenderer.SetEnvironmentListUpdateStatus, + 'ENV-DELETE-FINISHED' + ); const envs = await this._app.registry.getEnvironmentList(true); this.setPythonEnvironmentList(envs); } catch (error) { - // + this._window.window.webContents.send( + EventTypeRenderer.SetEnvironmentListUpdateStatus, + 'ENV-DELETE-FAILED', + `Failed to delete environment. ${error.message}` + ); } } } @@ -166,13 +185,33 @@ export class ManagePythonEnvironmentDialog { } #content-area { background: var(--neutral-layer-4); - display: flex; - flex-direction: row; - column-gap: 20px; - flex-grow: 1; + width: 100%; overflow-y: auto; margin: 5px; } + #env-list-progress { + width: 100%; + visibility: hidden; + } + #env-list-progress-message { + display: none; + border: 1px solid var(--error-fill-hover); + padding: 5px; + box-sizing: border-box; + } + #env-list-progress-message-content { + flex-grow: 1; + } + #env-list-progress-message-close { + cursor: pointer; + } + #env-list-progress-message-close svg { + width: 16px; + height: 16px; + } + #env-list-progress-message-close svg path { + fill: var(--error-fill-hover); + } jp-tree-item::part(start) { flex-grow: 1; } @@ -402,9 +441,18 @@ export class ManagePythonEnvironmentDialog {
-
- - +
+
+ +
+
+
+
${xMarkIconSrc}
+
+
+ + +
@@ -429,7 +477,7 @@ export class ManagePythonEnvironmentDialog {
-
${checkIconSrc}
${xMarkIconSrc}
+
${checkIconSrc}
${xMarkCircleIconSrc}
@@ -525,7 +573,7 @@ export class ManagePythonEnvironmentDialog {
-
${checkIconSrc}
${xMarkIconSrc}
+
${checkIconSrc}
${xMarkCircleIconSrc}
@@ -541,7 +589,7 @@ export class ManagePythonEnvironmentDialog {
-
${checkIconSrc}
${xMarkIconSrc}
+
${checkIconSrc}
${xMarkCircleIconSrc}
@@ -557,7 +605,7 @@ export class ManagePythonEnvironmentDialog {
-
${checkIconSrc}
${xMarkIconSrc}
+
${checkIconSrc}
${xMarkCircleIconSrc}
@@ -571,6 +619,9 @@ export class ManagePythonEnvironmentDialog {
-
- Restart session using a different Python environment +
@@ -115,7 +156,7 @@ export class PythonEnvironmentSelectPopup { const envListMenu = document.getElementById('env-list'); let envs = <%- JSON.stringify(envs) %>; let activeIndex = -1; - const envPaths = envs.map(env => env.path); + let envPaths = envs.map(env => env.path); let filteredEnvIndixes = []; const uf = new uFuzzy({ intraChars: "[A-Za-z0-9_]", @@ -159,6 +200,10 @@ export class PythonEnvironmentSelectPopup { envListMenu.innerHTML = html; } + function handleCopySessionInfo() { + window.electronAPI.copySessionInfo(); + } + window.electronAPI.onResetPythonEnvSelectPopup((path) => { pythonPathInput.value = ''; activeIndex = 0; @@ -173,6 +218,7 @@ export class PythonEnvironmentSelectPopup { window.electronAPI.onSetPythonEnvironmentList((newEnvs) => { envs = newEnvs; + envPaths = envs.map(env => env.path); pythonPathInput.value = ''; updateMenu(); }); @@ -313,6 +359,7 @@ export class PythonEnvironmentSelectPopup { } setPythonEnvironmentList(envs: IPythonEnvironment[]) { + this._envs = envs; this._view.view.webContents.send( EventTypeRenderer.SetPythonEnvironmentList, envs diff --git a/src/main/sessionwindow/sessionwindow.ts b/src/main/sessionwindow/sessionwindow.ts index 0cc4441b..e87383c0 100644 --- a/src/main/sessionwindow/sessionwindow.ts +++ b/src/main/sessionwindow/sessionwindow.ts @@ -945,6 +945,44 @@ export class SessionWindow implements IDisposable { } } ); + + this._evm.registerSyncEventHandler( + EventTypeMain.CopySessionInfoToClipboard, + event => { + if (event.sender !== this._envSelectPopup?.view?.view?.webContents) { + return; + } + + const serverInfo = this.getServerInfo(); + + let content = ''; + + if (serverInfo.type === 'local') { + content = [ + 'type: local server', + `url: ${serverInfo.url}`, + `server root: ${serverInfo.workingDirectory}`, + `env name: ${serverInfo.environment.name}`, + `env Python path: ${serverInfo.environment.path}`, + `versions: ${JSON.stringify(serverInfo.environment.versions)}` + ].join('\n'); + } else { + const url = new URL(serverInfo.url); + const isLocalUrl = + url.hostname === 'localhost' || url.hostname === '127.0.0.1'; + + content = [ + `type: ${isLocalUrl ? 'local' : 'remote'} connection`, + `url: ${serverInfo.url}`, + `session data: ${ + serverInfo.persistSessionData ? '' : 'not ' + }persisted` + ].join('\n'); + } + + clipboard.writeText(content); + } + ); } private _restartServerInPythonEnvironment(pythonPath: string) { @@ -976,7 +1014,7 @@ export class SessionWindow implements IDisposable { } } - async getServerInfo(): Promise { + getServerInfo(): IServerInfo { if (this._contentViewType !== ContentViewType.Lab) { return null; } @@ -1113,19 +1151,24 @@ export class SessionWindow implements IDisposable { } private async _createEnvSelectPopup() { - const envs = await this.registry.getEnvironmentList(false); const defaultEnv = await this._registry.getDefaultEnvironment(); const defaultPythonPath = defaultEnv ? defaultEnv.path : ''; this._envSelectPopup = new PythonEnvironmentSelectPopup({ app: this._app, isDarkTheme: this._isDarkTheme, - envs, + envs: [], // start with empty list, populate later bundledPythonPath: getBundledPythonPath(), defaultPythonPath }); this._envSelectPopup.load(); + + this.registry.getEnvironmentList(false).then(envs => { + this._envSelectPopup.setPythonEnvironmentList(envs); + this._resizeEnvSelectPopup(); + this._envSelectPopup.resetView(); + }); } private async _showEnvSelectPopup() { @@ -1141,7 +1184,6 @@ export class SessionWindow implements IDisposable { } } - // TODO: bug. this._envSelectPopup might still be loading this._envSelectPopup.setCurrentPythonPath(currentPythonPath); this._envSelectPopup.resetView(); From 8bdd6b6da865b16733676b6ee97eb347ad25c4af Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Mon, 1 Jan 2024 22:26:30 -0800 Subject: [PATCH 21/62] fix path issues on windows --- src/main/pythonenvdialog/pythonenvdialog.ts | 19 ++++++++----------- .../pythonenvselectpopup.ts | 8 ++++++-- src/main/sessionwindow/sessionwindow.ts | 14 +++++++++----- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/main/pythonenvdialog/pythonenvdialog.ts b/src/main/pythonenvdialog/pythonenvdialog.ts index 065f9cec..1f37388e 100644 --- a/src/main/pythonenvdialog/pythonenvdialog.ts +++ b/src/main/pythonenvdialog/pythonenvdialog.ts @@ -675,8 +675,8 @@ export class ManagePythonEnvironmentDialog { let systemPythonPath = <%- JSON.stringify(systemPythonPath) %>; let envs = <%- JSON.stringify(envs) %>; - const pythonEnvInstallPath = "<%- pythonEnvInstallPath %>"; - const pathSeparator = '${path.sep}'; + const pythonEnvInstallPath = <%- JSON.stringify(pythonEnvInstallPath) %>; + const pathSeparator = ${JSON.stringify(path.sep)}; const debounceWait = 200; let nameInputValid = false; let nameInputValidationTimer = -1; @@ -698,7 +698,7 @@ export class ManagePythonEnvironmentDialog { pythonPathInput.setAttribute('disabled', 'disabled'); selectPythonPathButton.setAttribute('disabled', 'disabled'); window.electronAPI.setDefaultPythonPath(''); - pythonPathInput.value = '${bundledPythonPath}'; + pythonPathInput.value = ${JSON.stringify(bundledPythonPath)}; validateAndUpdateCustomPythonPath(); } else { pythonPathInput.removeAttribute('disabled'); @@ -806,7 +806,11 @@ export class ManagePythonEnvironmentDialog { return env.name + '\\n' + env.path + '\\n' + packages.join(', '); } function getEnvTag(env) { - return env.path === "${defaultPythonPath}" ? ' (default)' : env.path === "${bundledPythonPath}" ? ' (bundled)' : ''; + return env.path === ${JSON.stringify( + defaultPythonPath + )} ? ' (default)' : env.path === ${JSON.stringify( + bundledPythonPath + )} ? ' (bundled)' : ''; } function generateEnvTypeList(name, envs) { @@ -961,13 +965,6 @@ export class ManagePythonEnvironmentDialog { if (!(status === 'REMOVING_EXISTING_INSTALLATION' || status === 'STARTED')) { clearCreateFormButton.style.display = 'block'; } - - if (status === 'SUCCESS') { - bundledEnvRadio.removeAttribute('disabled'); - hideBundledEnvWarning(); - } - - installBundledEnvButton.removeAttribute('disabled'); } async function handleInstallBundledPythonEnvStatusJupyterLabServerEnv(status, msg) { diff --git a/src/main/pythonenvselectpopup/pythonenvselectpopup.ts b/src/main/pythonenvselectpopup/pythonenvselectpopup.ts index a57fc1d6..6274e163 100644 --- a/src/main/pythonenvselectpopup/pythonenvselectpopup.ts +++ b/src/main/pythonenvselectpopup/pythonenvselectpopup.ts @@ -151,7 +151,7 @@ export class PythonEnvironmentSelectPopup {
@@ -604,20 +424,6 @@ export class SettingsDialog { const ctrlWBehavior = document.querySelector('jp-radio[name="ctrl-w-behavior"].checked').value; window.electronAPI.setCtrlWBehavior(ctrlWBehavior); - if (defaultPythonEnvChanged) { - if (bundledEnvRadio.checked) { - window.electronAPI.setDefaultPythonPath(''); - } else { - window.electronAPI.validatePythonPath(pythonPathInput.value).then((valid) => { - if (valid) { - window.electronAPI.setDefaultPythonPath(pythonPathInput.value); - } else { - window.electronAPI.showInvalidPythonPathMessage(pythonPathInput.value); - } - }); - } - } - window.electronAPI.restartApp(); } @@ -642,10 +448,6 @@ export class SettingsDialog { installUpdatesAutomaticallyEnabled, installUpdatesAutomatically, defaultWorkingDirectory, - defaultPythonPath, - selectBundledPythonPath, - bundledEnvInstallationExists, - bundledEnvInstallationLatest, logLevel, serverArgs, overrideDefaultServerArgs, @@ -682,7 +484,6 @@ export namespace SettingsDialog { checkForUpdatesAutomatically: boolean; installUpdatesAutomatically: boolean; defaultWorkingDirectory: string; - defaultPythonPath: string; activateTab?: Tab; logLevel: LogLevel; serverArgs: string; From ae8fe84d868d0c2326c34150c01e39abc1718b83 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Thu, 4 Jan 2024 19:19:09 -0800 Subject: [PATCH 37/62] improve env list panel layout --- src/main/pythonenvdialog/pythonenvdialog.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/pythonenvdialog/pythonenvdialog.ts b/src/main/pythonenvdialog/pythonenvdialog.ts index e73a1d8c..a95a6d9c 100644 --- a/src/main/pythonenvdialog/pythonenvdialog.ts +++ b/src/main/pythonenvdialog/pythonenvdialog.ts @@ -193,7 +193,7 @@ export class ManagePythonEnvironmentDialog { } #env-list-progress { width: 100%; - visibility: hidden; + display: none; } #env-list-progress-message { display: none; @@ -254,6 +254,7 @@ export class ManagePythonEnvironmentDialog { flex-direction: column; align-items: baseline; padding-bottom: 10px; + padding-right: 5px; } .setting-section .header { line-height: 30px; @@ -263,6 +264,9 @@ export class ManagePythonEnvironmentDialog { .setting-section jp-text-field { width: 100%; } + .setting-section.env-list-section { + overflow-y: auto; + } jp-tab-panel .setting-section:last-child { border-bottom: none; } @@ -393,7 +397,7 @@ export class ManagePythonEnvironmentDialog {
-
+
@@ -689,7 +693,7 @@ export class ManagePythonEnvironmentDialog { } function showEnvListProgress(show) { - envListProgress.style.visibility = show ? 'visible' : 'hidden'; + envListProgress.style.display = show ? 'block' : 'none'; } function setEnvListProgressMessage(message) { From 7b8a90f652218f07ceccf6975c02a2e69564cc11 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Thu, 4 Jan 2024 20:29:43 -0800 Subject: [PATCH 38/62] fix styling issues on linux --- src/main/pythonenvdialog/pythonenvdialog.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/pythonenvdialog/pythonenvdialog.ts b/src/main/pythonenvdialog/pythonenvdialog.ts index a95a6d9c..e7b2bbbc 100644 --- a/src/main/pythonenvdialog/pythonenvdialog.ts +++ b/src/main/pythonenvdialog/pythonenvdialog.ts @@ -188,8 +188,6 @@ export class ManagePythonEnvironmentDialog { #content-area { background: var(--neutral-layer-4); width: 100%; - overflow-y: auto; - margin: 5px; } #env-list-progress { width: 100%; @@ -200,6 +198,7 @@ export class ManagePythonEnvironmentDialog { border: 1px solid var(--error-fill-hover); padding: 5px; box-sizing: border-box; + margin-bottom: 5px; } #env-list-progress-message-content { flex-grow: 1; @@ -254,7 +253,6 @@ export class ManagePythonEnvironmentDialog { flex-direction: column; align-items: baseline; padding-bottom: 10px; - padding-right: 5px; } .setting-section .header { line-height: 30px; From e14d7f86e873518b05205c2167f2a4de36843813 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Thu, 4 Jan 2024 21:46:49 -0800 Subject: [PATCH 39/62] fix popup initialization issue on first launch --- src/main/sessionwindow/sessionwindow.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/sessionwindow/sessionwindow.ts b/src/main/sessionwindow/sessionwindow.ts index a857fe82..10e721d5 100644 --- a/src/main/sessionwindow/sessionwindow.ts +++ b/src/main/sessionwindow/sessionwindow.ts @@ -1177,7 +1177,13 @@ export class SessionWindow implements IDisposable { } private async _createEnvSelectPopup() { - const defaultEnv = await this._registry.getDefaultEnvironment(); + let defaultEnv: IPythonEnvironment; + try { + defaultEnv = await this._registry.getDefaultEnvironment(); + } catch (error) { + // + } + const defaultPythonPath = defaultEnv ? defaultEnv.path : ''; this._envSelectPopup = new PythonEnvironmentSelectPopup({ From dacf891112da2bfd745c0e074ccad62fceadc812 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Thu, 4 Jan 2024 22:00:01 -0800 Subject: [PATCH 40/62] handle exception when using appData.pythonPath --- src/main/registry.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/main/registry.ts b/src/main/registry.ts index 34317c4a..a61446b0 100644 --- a/src/main/registry.ts +++ b/src/main/registry.ts @@ -71,6 +71,8 @@ export class Registry implements IRegistry, IDisposable { ].filter(env => this._pathExistsSync(env.path)); // initialize default environment to user set or bundled + // TODO: try to use userSettings.pythonPath and bundled python path separately + // because userSettings.pythonPath might be invalid but bundled one may be valid let pythonPath = userSettings.getValue(SettingType.pythonPath); if (pythonPath === '') { pythonPath = getBundledPythonPath(); @@ -100,19 +102,23 @@ export class Registry implements IRegistry, IDisposable { // try to set default env from discovered pythonPath if (!this._defaultEnv && appData.pythonPath) { - const defaultEnv = this._resolveEnvironmentSync(appData.pythonPath); - - if (defaultEnv) { - this._defaultEnv = defaultEnv; - // if default env is conda root, then set its conda executable as conda path - if ( - defaultEnv.type === IEnvironmentType.CondaRoot && - !appData.condaPath - ) { - this.setCondaPath( - condaExePathForEnvPath(getEnvironmentPath(defaultEnv)) - ); + try { + const defaultEnv = this._resolveEnvironmentSync(appData.pythonPath); + + if (defaultEnv) { + this._defaultEnv = defaultEnv; + // if default env is conda root, then set its conda executable as conda path + if ( + defaultEnv.type === IEnvironmentType.CondaRoot && + !appData.condaPath + ) { + this.setCondaPath( + condaExePathForEnvPath(getEnvironmentPath(defaultEnv)) + ); + } } + } catch (error) { + // } } From c1cdd78798ee2a8757b7b3f0002fd2cd1843046b Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Fri, 5 Jan 2024 09:23:38 -0800 Subject: [PATCH 41/62] highlight current env, fix issue with currentPythonPath calculation --- .../pythonenvselectpopup.ts | 19 +++++++++++++++++-- src/main/sessionwindow/sessionwindow.ts | 11 ++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/main/pythonenvselectpopup/pythonenvselectpopup.ts b/src/main/pythonenvselectpopup/pythonenvselectpopup.ts index cc74abde..fadec30a 100644 --- a/src/main/pythonenvselectpopup/pythonenvselectpopup.ts +++ b/src/main/pythonenvselectpopup/pythonenvselectpopup.ts @@ -198,19 +198,30 @@ export class PythonEnvironmentSelectPopup { function updateMenu(filterOrder, filterInfo) { let html = ''; + activeIndex = 0; if (filterOrder && filterInfo) { for (let i = 0; i < filterOrder.length; i++) { const infoIdx = filterOrder[i]; - html += generateMenuItem(envs[filterInfo.idx[infoIdx]], filterInfo.ranges[infoIdx]); + const env = envs[filterInfo.idx[infoIdx]]; + if (env.path === currentPythonPath) { + activeIndex = i; + } + html += generateMenuItem(env, filterInfo.ranges[infoIdx]); } } else { - for (const env of envs) { + for (let i = 0; i < envs.length; i++) { + const env = envs[i]; + if (env.path === currentPythonPath) { + activeIndex = i; + } html += generateMenuItem(env); } } envListMenu.innerHTML = html; + + updateActiveItem(); } function handleRestartSession() { @@ -255,6 +266,10 @@ export class PythonEnvironmentSelectPopup { item.classList.remove('active'); }); + if (activeIndex < 0 || activeIndex >= envListMenu.children.length) { + return; + } + const activeItem = envListMenu.children[activeIndex]; activeItem.scrollIntoView(); activeItem.classList.add('active'); diff --git a/src/main/sessionwindow/sessionwindow.ts b/src/main/sessionwindow/sessionwindow.ts index 10e721d5..295b0da4 100644 --- a/src/main/sessionwindow/sessionwindow.ts +++ b/src/main/sessionwindow/sessionwindow.ts @@ -1212,15 +1212,8 @@ export class SessionWindow implements IDisposable { return; } - let currentPythonPath = this._wsSettings.getValue(SettingType.pythonPath); - if (!currentPythonPath) { - const defaultEnv = await this.registry.getDefaultEnvironment(); - if (defaultEnv) { - currentPythonPath = defaultEnv.path; - } - } - - this._envSelectPopup.setCurrentPythonPath(currentPythonPath); + const serverInfo = this.getServerInfo(); + this._envSelectPopup.setCurrentPythonPath(serverInfo?.environment?.path); this._envSelectPopup.resetView(); this._window.addBrowserView(this._envSelectPopup.view.view); From 5ea08af216da828fc03462a2dc810e3518b1c5b1 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Fri, 5 Jan 2024 19:52:38 -0800 Subject: [PATCH 42/62] show detailed env validation error message --- src/main/pythonenvdialog/pythonenvdialog.ts | 30 ++++++- src/main/registry.ts | 89 ++++++++++++++++----- src/main/tokens.ts | 6 ++ 3 files changed, 101 insertions(+), 24 deletions(-) diff --git a/src/main/pythonenvdialog/pythonenvdialog.ts b/src/main/pythonenvdialog/pythonenvdialog.ts index e7b2bbbc..bbe7cdbd 100644 --- a/src/main/pythonenvdialog/pythonenvdialog.ts +++ b/src/main/pythonenvdialog/pythonenvdialog.ts @@ -956,14 +956,35 @@ export class ManagePythonEnvironmentDialog { } }); + function getEnvironmentValidationErrorMessage(error) { + let message = 'Invalid Python path selected'; + + if (!error) { + return message; + } + + if (error.type === 'path-not-found') { + message = 'File not found at the selected path'; + } else if (error.type === 'invalid-python-binary') { + message = 'File selected is not a Python binary'; + } else if (error.type === 'requirements-not-satisfied') { + message = 'Required Python package (jupyterlab) not found in the selected environment. Install in the selected environment and retry.'; + } else if (error.type === PythonEnvResolveErrorType.ResolveError) { + message = 'Failed to get environment information at selected path'; + } + + return message; + } + async function handleCustomPythonPathSelectedForAddExistingEnv(path) { showEnvListProgress(true); const inRegistry = await window.electronAPI.getEnvironmentByPythonPath(path); if (!inRegistry) { - if (await window.electronAPI.validatePythonPath(path)) { + const validateResult = await window.electronAPI.validatePythonPath(path); + if (validateResult.valid) { await window.electronAPI.addEnvironmentByPythonPath(path); } else { - setEnvListProgressMessage('Invalid Python path selected.'); + setEnvListProgressMessage(getEnvironmentValidationErrorMessage(validateResult.error)); } } else { setEnvListProgressMessage('Environment is already in registry.'); @@ -1091,12 +1112,13 @@ export class ManagePythonEnvironmentDialog { if (inRegistry) { valid = true; } else { - if (await window.electronAPI.validatePythonPath(pythonPath)) { + const validateResult = await window.electronAPI.validatePythonPath(pythonPath); + if (validateResult.valid) { await window.electronAPI.addEnvironmentByPythonPath(pythonPath); valid = true; } else { valid = false; - message = 'Invalid Python path. Make sure "jupyterlab" Python package is installed in the environment.'; + message = getEnvironmentValidationErrorMessage(validateResult.error); } } if (valid && !bundledEnvRadio.checked) { diff --git a/src/main/registry.ts b/src/main/registry.ts index a61446b0..e488db3a 100644 --- a/src/main/registry.ts +++ b/src/main/registry.ts @@ -10,6 +10,7 @@ import { IEnvironmentType, IPythonEnvironment, IPythonEnvResolveError, + IPythonEnvValidateResult, IVersionContainer, PythonEnvResolveErrorType } from './tokens'; @@ -45,7 +46,9 @@ export interface IRegistry { getEnvironmentList: (cacheOK: boolean) => Promise; addEnvironment: (pythonPath: string) => IPythonEnvironment; removeEnvironment: (pythonPath: string) => boolean; - validatePythonEnvironmentAtPath: (pythonPath: string) => Promise; + validatePythonEnvironmentAtPath: ( + pythonPath: string + ) => Promise; validateCondaBaseEnvironmentAtPath: (envPath: string) => boolean; setDefaultPythonPath: (pythonPath: string) => boolean; getCurrentPythonEnvironment: () => IPythonEnvironment; @@ -263,6 +266,11 @@ export class Registry implements IRegistry, IDisposable { } } + /** + * Resolve Python environment at pythonPath + * @param pythonPath Python path + * @returns Python environment info or throws exception PythonEnvResolveErrorType + */ private _resolveEnvironmentSync(pythonPath: string): IPythonEnvironment { if (!this._pathExistsSync(pythonPath)) { log.error(`Python path "${pythonPath}" does not exist.`); @@ -392,17 +400,20 @@ export class Registry implements IRegistry, IDisposable { return inUserSetEnvList; } - const env = this._resolveEnvironmentSync(pythonPath); - if (env) { - if (!this._defaultEnv) { - this._defaultEnv = env; + try { + const env = this._resolveEnvironmentSync(pythonPath); + if (env) { + if (!this._defaultEnv) { + this._defaultEnv = env; + } + this._userSetEnvironments.push(env); + this._updateEnvironments(); + this._environmentListUpdated.emit(); + return env; } - this._userSetEnvironments.push(env); - this._updateEnvironments(); - this._environmentListUpdated.emit(); + } catch (error) { + // } - - return env; } /** @@ -433,12 +444,46 @@ export class Registry implements IRegistry, IDisposable { return false; } - async validatePythonEnvironmentAtPath(pythonPath: string): Promise { - const isValidPythonBinary = (await validatePythonPath(pythonPath)).valid; - return ( - isValidPythonBinary && - (await this._resolveEnvironment(pythonPath)) !== undefined - ); + async validatePythonEnvironmentAtPath( + pythonPath: string + ): Promise { + if (!fs.existsSync(pythonPath)) { + return { + valid: false, + error: { + type: PythonEnvResolveErrorType.PathNotFound + } + }; + } + if (!(await validatePythonPath(pythonPath)).valid) { + return { + valid: false, + error: { + type: PythonEnvResolveErrorType.InvalidPythonBinary + } + }; + } + + try { + const env = this._resolveEnvironmentSync(pythonPath); + if (!env) { + return { + valid: false, + error: { + type: PythonEnvResolveErrorType.ResolveError + } + }; + } + } catch (error) { + return { + valid: false, + error + }; + } + + return { + valid: true + }; } validateCondaBaseEnvironmentAtPath(envPath: string): boolean { @@ -474,10 +519,14 @@ export class Registry implements IRegistry, IDisposable { } // try to resolve it - env = this._resolveEnvironmentSync(pythonPath); - if (env) { - this._defaultEnv = env; - return true; + try { + env = this._resolveEnvironmentSync(pythonPath); + if (env) { + this._defaultEnv = env; + return true; + } + } catch (error) { + // } return false; diff --git a/src/main/tokens.ts b/src/main/tokens.ts index 7bc37bd0..667010ec 100644 --- a/src/main/tokens.ts +++ b/src/main/tokens.ts @@ -66,6 +66,7 @@ export interface IPythonEnvironment { export enum PythonEnvResolveErrorType { PathNotFound = 'path-not-found', + InvalidPythonBinary = 'invalid-python-binary', ResolveError = 'resolve-error', RequirementsNotSatisfied = 'requirements-not-satisfied' } @@ -75,6 +76,11 @@ export interface IPythonEnvResolveError { message?: string; } +export interface IPythonEnvValidateResult { + valid: boolean; + error?: IPythonEnvResolveError; +} + export interface IDisposable { dispose(): Promise; } From 348c2cad0d5aa3b5f26b156bb003013e5ef327af Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Fri, 5 Jan 2024 20:29:05 -0800 Subject: [PATCH 43/62] improve package list input check --- src/main/app.ts | 17 +++++++++++++++++ src/main/pythonenvdialog/pythonenvdialog.ts | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/app.ts b/src/main/app.ts index 7140dfc3..e8aa8d9a 100644 --- a/src/main/app.ts +++ b/src/main/app.ts @@ -1022,6 +1022,23 @@ export class JupyterApplication implements IApplication, IDisposable { return; } + // still check input to prevent chaining malicious commands + const invalidCharInputRegex = new RegExp('[&;|]'); + const invalidInputMessage = invalidCharInputRegex.test(envPath) + ? 'Invalid environment name input' + : invalidCharInputRegex.test(packages) + ? 'Invalid package list input' + : ''; + + if (invalidInputMessage) { + event.sender.send( + EventTypeRenderer.InstallPythonEnvStatus, + EnvironmentInstallStatus.Failure, + invalidInputMessage + ); + return; + } + event.sender.send( EventTypeRenderer.InstallPythonEnvStatus, EnvironmentInstallStatus.Started diff --git a/src/main/pythonenvdialog/pythonenvdialog.ts b/src/main/pythonenvdialog/pythonenvdialog.ts index bbe7cdbd..44b7c975 100644 --- a/src/main/pythonenvdialog/pythonenvdialog.ts +++ b/src/main/pythonenvdialog/pythonenvdialog.ts @@ -910,7 +910,7 @@ export class ManagePythonEnvironmentDialog { status === 'CANCELLED' ? 'Installation cancelled!' : status === 'FAILURE' ? - 'Failed to install the environment!' : + msg || 'Failed to install the environment!' : status === 'SUCCESS' ? 'Installation succeeded' : ''; const animate = status === 'REMOVING_EXISTING_INSTALLATION' From 36c42b9739610a70cf5e4944fe773344c7f4ae79 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Sat, 6 Jan 2024 16:26:32 -0800 Subject: [PATCH 44/62] launch terminal and show in finder --- src/main/pythonenvdialog/pythonenvdialog.ts | 27 ++++++++++++ src/main/utils.ts | 46 +++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/main/pythonenvdialog/pythonenvdialog.ts b/src/main/pythonenvdialog/pythonenvdialog.ts index 44b7c975..6977ee37 100644 --- a/src/main/pythonenvdialog/pythonenvdialog.ts +++ b/src/main/pythonenvdialog/pythonenvdialog.ts @@ -14,16 +14,20 @@ import * as fs from 'fs'; import { ThemedWindow } from '../dialog/themedwindow'; import { IPythonEnvironment } from '../tokens'; import { + createCommandScriptInEnv, deletePythonEnvironment, envPathForPythonPath, getBundledPythonPath, isEnvInstalledByDesktopApp, + launchTerminalInDirectory, + openDirectoryInExplorer, versionWithoutSuffix } from '../utils'; import { EventManager } from '../eventmanager'; import { EventTypeMain, EventTypeRenderer } from '../eventtypes'; import { JupyterApplication } from '../app'; import { + condaEnvPathForCondaExePath, getCondaPath, getNextPythonEnvName, getPythonEnvsDirectory, @@ -111,6 +115,10 @@ export class ManagePythonEnvironmentDialog { const deletable = installedByApp && !this._app.serverFactory.isEnvironmentInUse(pythonPath); + const openInExplorerLabel = + process.platform === 'darwin' + ? 'Reveal in Finder' + : 'Open in Explorer'; const template: MenuItemConstructorOptions[] = [ { label: 'Copy Python path', @@ -137,6 +145,25 @@ export class ManagePythonEnvironmentDialog { } } }, + { + label: 'Launch Terminal', + click: () => { + const condaPath = getCondaPath() || ''; + const condaEnvPath = condaEnvPathForCondaExePath(condaPath); + let activateCommand = createCommandScriptInEnv( + envPath, + condaEnvPath + ); + + launchTerminalInDirectory(envPath, activateCommand); + } + }, + { + label: openInExplorerLabel, + click: () => { + openDirectoryInExplorer(envPath); + } + }, { type: 'separator', visible: deletable }, { label: 'Delete', diff --git a/src/main/utils.ts b/src/main/utils.ts index 6da2816e..d477beae 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -616,3 +616,49 @@ export function runCommandSync( ): string { return execFileSync(executablePath, commands, options).toString(); } + +export function openDirectoryInExplorer(dirPath: string): boolean { + if (!(fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory())) { + return false; + } + + const { platform } = process; + const openCommand = + platform === 'darwin' + ? 'open' + : platform === 'win32' + ? 'explorer' + : 'xdg-open'; + + exec(`${openCommand} "${dirPath}"`); + + return true; +} + +export function launchTerminalInDirectory( + dirPath: string, + commands?: string +): boolean { + if (!(fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory())) { + return false; + } + + const { platform } = process; + let callCommands = ''; + if (commands) { + // replace " with ' + commands = commands.split('"').join("'"); + callCommands = `&& ${commands}`; + } + + if (platform === 'darwin') { + exec( + `osascript -e 'tell application "Terminal" to do script "cd '${dirPath}' ${callCommands}"' -e 'tell application "Terminal" to activate'` + ); + ``; + } else if (platform === 'win32') { + exec(`start cmd.exe /K cd /D '${dirPath}' ${callCommands}`); + } else { + // + } +} From 32afe74aae2c6c708e608ae6d37fdf4d60cb4cc5 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Sat, 6 Jan 2024 16:51:43 -0800 Subject: [PATCH 45/62] fix terminal launch in win --- src/main/utils.ts | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/main/utils.ts b/src/main/utils.ts index d477beae..6468b729 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -644,20 +644,37 @@ export function launchTerminalInDirectory( } const { platform } = process; - let callCommands = ''; - if (commands) { - // replace " with ' - commands = commands.split('"').join("'"); - callCommands = `&& ${commands}`; - } - if (platform === 'darwin') { + let callCommands = ''; + if (commands) { + // replace " with ' + commands = commands.split('"').join("'"); + callCommands = `&& ${commands}`; + } + exec( `osascript -e 'tell application "Terminal" to do script "cd '${dirPath}' ${callCommands}"' -e 'tell application "Terminal" to activate'` ); ``; } else if (platform === 'win32') { - exec(`start cmd.exe /K cd /D '${dirPath}' ${callCommands}`); + if (commands) { + const activateFilePath = createTempFile( + `activate.bat`, + `cd /D "${dirPath}"\n${commands}` + ); + + exec(`start cmd.exe /K ${activateFilePath}`); + + setTimeout(() => { + try { + fs.unlinkSync(activateFilePath); + } catch (error) { + console.error('Failed to delete the temp file'); + } + }, 2000); + } else { + exec(`start cmd.exe /K cd /D "${dirPath}"`); + } } else { // } From fce4987903275f3f9661c164ecfddd84ef6bc158 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Sat, 6 Jan 2024 17:37:47 -0800 Subject: [PATCH 46/62] linux launch terminal implementation --- src/main/pythonenvdialog/pythonenvdialog.ts | 2 +- src/main/utils.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/pythonenvdialog/pythonenvdialog.ts b/src/main/pythonenvdialog/pythonenvdialog.ts index 6977ee37..900ad225 100644 --- a/src/main/pythonenvdialog/pythonenvdialog.ts +++ b/src/main/pythonenvdialog/pythonenvdialog.ts @@ -150,7 +150,7 @@ export class ManagePythonEnvironmentDialog { click: () => { const condaPath = getCondaPath() || ''; const condaEnvPath = condaEnvPathForCondaExePath(condaPath); - let activateCommand = createCommandScriptInEnv( + const activateCommand = createCommandScriptInEnv( envPath, condaEnvPath ); diff --git a/src/main/utils.ts b/src/main/utils.ts index 6468b729..eaadcab4 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -676,6 +676,10 @@ export function launchTerminalInDirectory( exec(`start cmd.exe /K cd /D "${dirPath}"`); } } else { - // + let callCommands = ''; + if (commands) { + callCommands = ` -- bash -c "${commands}; exec bash"`; + } + exec(`gnome-terminal --working-directory="${dirPath}"${callCommands}`); } } From dcb57fdd34022bd89717996aae622887af597010 Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Sun, 7 Jan 2024 12:28:34 -0800 Subject: [PATCH 47/62] CLI updates to sync with env management UI --- .vscode/launch.json | 15 ++++++++++- src/main/cli.ts | 62 +++++++++++++++++++++++++++++++++------------ 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 8f30393e..3672c4af 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -25,7 +25,20 @@ "env": { "NODE_ENV": "development" } - } + }, + { + "name": "Debug CLI", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", + "args" : [".", "env", "list"], + "outputCapture": "std", + "preLaunchTask": "npm: build", + "env": { + "NODE_ENV": "development" + } + } ] } \ No newline at end of file diff --git a/src/main/cli.ts b/src/main/cli.ts index a3a329b9..bec1cb12 100644 --- a/src/main/cli.ts +++ b/src/main/cli.ts @@ -24,7 +24,9 @@ import { condaEnvPathForCondaExePath, getPythonEnvsDirectory, ICommandRunCallbacks, - runCommandInEnvironment + runCommandInEnvironment, + validateCondaPath, + validateSystemPythonPath } from './env'; export function parseCLIArgs(argv: string[]) { @@ -139,6 +141,9 @@ export function parseCLIArgs(argv: string[]) { case 'set-conda-path': await handleEnvSetCondaPathCommand(argv); break; + case 'set-system-python-path': + await handleEnvSetSystemPythonPathCommand(argv); + break; case 'update-registry': await handleEnvUpdateRegistryCommand(argv); break; @@ -152,9 +157,11 @@ export function parseCLIArgs(argv: string[]) { } export async function handleEnvInfoCommand(argv: any) { - const bundledEnvPath = getBundledPythonEnvPath(); - const bundledEnvPathExists = - fs.existsSync(bundledEnvPath) && fs.statSync(bundledEnvPath).isDirectory(); + const bundledPythonPath = getBundledPythonPath(); + const bundledPythonPathExists = + fs.existsSync(bundledPythonPath) && + (fs.statSync(bundledPythonPath).isFile() || + fs.statSync(bundledPythonPath).isSymbolicLink()); let defaultPythonPath = userSettings.getValue(SettingType.pythonPath); if (!defaultPythonPath) { defaultPythonPath = getBundledPythonPath(); @@ -162,9 +169,10 @@ export async function handleEnvInfoCommand(argv: any) { if (!fs.existsSync(defaultPythonPath)) { defaultPythonPath = appData.pythonPath; } - const defaultEnvPath = envPathForPythonPath(defaultPythonPath); - const defaultEnvPathExists = - fs.existsSync(defaultEnvPath) && fs.statSync(defaultEnvPath).isDirectory(); + const defaultPythonPathExists = + fs.existsSync(defaultPythonPath) && + (fs.statSync(defaultPythonPath).isFile() || + fs.statSync(defaultPythonPath).isSymbolicLink()); const condaPath = appData.condaPath; const condaPathExists = condaPath && fs.existsSync(condaPath) && fs.statSync(condaPath).isFile(); @@ -179,13 +187,13 @@ export async function handleEnvInfoCommand(argv: any) { const infoLines: string[] = []; infoLines.push( - `Default Python environment path:\n "${defaultEnvPath}" [${ - defaultEnvPathExists ? 'exists' : 'not found' + `Default Python path for JupyterLab Server:\n "${defaultPythonPath}" [${ + defaultPythonPathExists ? 'exists' : 'not found' }]` ); infoLines.push( - `Bundled Python environment installation path:\n "${bundledEnvPath}" [${ - bundledEnvPathExists ? 'exists' : 'not found' + `Bundled Python installation path:\n "${bundledPythonPath}" [${ + bundledPythonPathExists ? 'exists' : 'not found' }]` ); infoLines.push( @@ -218,7 +226,7 @@ export async function handleEnvListCommand(argv: any) { const envPath = envPathForPythonPath(env.path); const installedByApp = isEnvInstalledByDesktopApp(envPath); listLines.push( - ` [${env.name}], path: ${envPath}${ + ` [${env.name}], Python path: ${env.path}${ installedByApp ? ', installed by JupyterLab Desktop' : '' }\n packages: ${versions.join(', ')}` ); @@ -330,6 +338,13 @@ export async function handleEnvActivateCommand(argv: any) { envPath = getBundledPythonEnvPath(); } + if ( + !(envPath && fs.existsSync(envPath) && fs.statSync(envPath).isDirectory()) + ) { + console.error(`Invalid environment directory "${envPath}"`); + return; + } + console.log(`Activating Python environment "${envPath}"`); await launchCLIinEnvironment(envPath); @@ -451,14 +466,29 @@ export async function handleEnvSetCondaPathCommand(argv: any) { if (!fs.existsSync(condaPath)) { console.error(`conda path "${condaPath}" does not exist`); return; - } else if (!isBaseCondaEnv(condaEnvPathForCondaExePath(condaPath))) { - console.error(`"${condaPath}" is not in a base conda environemnt`); + } else if (!(await validateCondaPath(condaPath)).valid) { + console.error(`"${condaPath}" is not a valid conda path`); return; } console.log(`Setting "${condaPath}" as the conda path`); - appData.condaPath = condaPath; - appData.save(); + userSettings.setValue(SettingType.condaPath, condaPath); + userSettings.save(); +} + +export async function handleEnvSetSystemPythonPathCommand(argv: any) { + const systemPythonPath = argv.path as string; + if (!fs.existsSync(systemPythonPath)) { + console.error(`Python path "${systemPythonPath}" does not exist`); + return; + } else if (!(await validateSystemPythonPath(systemPythonPath)).valid) { + console.error(`"${systemPythonPath}" is not a valid Python path`); + return; + } + + console.log(`Setting "${systemPythonPath}" as the system Python path`); + userSettings.setValue(SettingType.systemPythonPath, systemPythonPath); + userSettings.save(); } export async function launchCLIinEnvironment( From b1219292ff1c0fe99017066f60eb809f60bdd3dc Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Sun, 7 Jan 2024 17:46:33 -0800 Subject: [PATCH 48/62] add env to registry only if it includes jupyterlab, fix linux menu separator visibility issue --- src/main/cli.ts | 5 ++++- src/main/pythonenvdialog/pythonenvdialog.ts | 16 +++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/main/cli.ts b/src/main/cli.ts index bec1cb12..70505416 100644 --- a/src/main/cli.ts +++ b/src/main/cli.ts @@ -395,7 +395,10 @@ export async function createPythonEnvironment( source: 'registry', appVersion: app.getVersion() }); - addUserSetEnvironment(envPath, isConda); + + if (packages.includes('jupyterlab')) { + addUserSetEnvironment(envPath, isConda); + } } export async function handleEnvCreateCommand(argv: any) { diff --git a/src/main/pythonenvdialog/pythonenvdialog.ts b/src/main/pythonenvdialog/pythonenvdialog.ts index 900ad225..bb81f31f 100644 --- a/src/main/pythonenvdialog/pythonenvdialog.ts +++ b/src/main/pythonenvdialog/pythonenvdialog.ts @@ -119,7 +119,7 @@ export class ManagePythonEnvironmentDialog { process.platform === 'darwin' ? 'Reveal in Finder' : 'Open in Explorer'; - const template: MenuItemConstructorOptions[] = [ + const envMenuTemplate: MenuItemConstructorOptions[] = [ { label: 'Copy Python path', click: () => { @@ -163,11 +163,13 @@ export class ManagePythonEnvironmentDialog { click: () => { openDirectoryInExplorer(envPath); } - }, - { type: 'separator', visible: deletable }, + } + ]; + + const deletableEnvMenuItems: MenuItemConstructorOptions[] = [ + { type: 'separator' }, { label: 'Delete', - visible: deletable, click: async () => { this._window.window.webContents.send( EventTypeRenderer.SetEnvironmentListUpdateStatus, @@ -195,7 +197,11 @@ export class ManagePythonEnvironmentDialog { } ]; - const menu = Menu.buildFromTemplate(template); + const menu = Menu.buildFromTemplate( + deletable + ? [...envMenuTemplate, ...deletableEnvMenuItems] + : envMenuTemplate + ); menu.popup({ window: BrowserWindow.fromWebContents(event.sender) }); From 86c6f656ccc30d41e18241206005ed4ce216d54e Mon Sep 17 00:00:00 2001 From: Mehmet Bektas Date: Thu, 18 Jan 2024 21:18:04 -0800 Subject: [PATCH 49/62] conda channels setting, new CLI options, env list load progress message --- src/main/app.ts | 37 ++- src/main/cli.ts | 295 +++++++++++++++--- src/main/config/settings.ts | 6 +- src/main/env.ts | 30 +- src/main/eventtypes.ts | 2 + src/main/pythonenvdialog/preload.ts | 9 + src/main/pythonenvdialog/pythonenvdialog.ts | 54 +++- .../pythonenvselectpopup.ts | 21 +- src/main/registry.ts | 5 +- src/main/utils.ts | 101 ++++-- 10 files changed, 481 insertions(+), 79 deletions(-) diff --git a/src/main/app.ts b/src/main/app.ts index e8aa8d9a..20927e07 100644 --- a/src/main/app.ts +++ b/src/main/app.ts @@ -56,6 +56,7 @@ import { addUserSetEnvironment, createPythonEnvironment } from './cli'; import { getNextPythonEnvName, JUPYTER_ENV_REQUIREMENTS, + validateCondaChannels, validateCondaPath, validateNewPythonEnvironmentName, validatePythonEnvironmentInstallDirectory, @@ -795,6 +796,16 @@ export class JupyterApplication implements IApplication, IDisposable { } ); + this._evm.registerEventHandler( + EventTypeMain.SetCondaChannels, + async (event, condaChannels) => { + const channelList = + condaChannels.trim() === '' ? [] : condaChannels.split(' '); + userSettings.setValue(SettingType.condaChannels, channelList); + userSettings.save(); + } + ); + this._evm.registerEventHandler( EventTypeMain.SetSystemPythonPath, async (event, pythonPath) => { @@ -919,6 +930,13 @@ export class JupyterApplication implements IApplication, IDisposable { } ); + this._evm.registerSyncEventHandler( + EventTypeMain.ValidateCondaChannels, + (event, condaChannels) => { + return Promise.resolve(validateCondaChannels(condaChannels)); + } + ); + this._evm.registerSyncEventHandler( EventTypeMain.ValidateSystemPythonPath, (event, pythonPath) => { @@ -1044,13 +1062,18 @@ export class JupyterApplication implements IApplication, IDisposable { EnvironmentInstallStatus.Started ); try { - await createPythonEnvironment(envPath, envType, packages, { - stdout: (msg: string) => { - event.sender.send( - EventTypeRenderer.InstallPythonEnvStatus, - EnvironmentInstallStatus.Running, - msg - ); + await createPythonEnvironment({ + envPath, + envType, + packageList: packages.split(' '), + callbacks: { + stdout: (msg: string) => { + event.sender.send( + EventTypeRenderer.InstallPythonEnvStatus, + EnvironmentInstallStatus.Running, + msg + ); + } } }); const pythonPath = pythonPathForEnvPath(envPath); diff --git a/src/main/cli.ts b/src/main/cli.ts index 70505416..297fe154 100644 --- a/src/main/cli.ts +++ b/src/main/cli.ts @@ -4,9 +4,10 @@ import { createTempFile, EnvironmentInstallStatus, envPathForPythonPath, + getBundledEnvInstallerPath, getBundledPythonEnvPath, getBundledPythonPath, - installBundledEnvironment, + installCondaPackEnvironment, isBaseCondaEnv, isEnvInstalledByDesktopApp, markEnvironmentAsJupyterInstalled, @@ -22,6 +23,7 @@ import { Registry } from './registry'; import { app } from 'electron'; import { condaEnvPathForCondaExePath, + getCondaChannels, getPythonEnvsDirectory, ICommandRunCallbacks, runCommandInEnvironment, @@ -45,15 +47,15 @@ export function parseCLIArgs(argv: string[]) { 'Launch in /data/nb and open /data/nb/test.ipynb and /data/nb/sub/test2.ipynb' ) .example( - 'jlab env install', + 'jlab env create', 'Install bundled Python environment to the default path' ) .example( - 'jlab env install --path /opt/jlab_server', + 'jlab env create --source bundle --prefix /opt/jlab_server', 'Install bundled Python environment to /opt/jlab_server' ) .example( - 'jlab env create --path /opt/jlab_server', + 'jlab env create --prefix /opt/jlab_server', 'Create new Python environment at /opt/jlab_server' ) .example( @@ -80,10 +82,13 @@ export function parseCLIArgs(argv: string[]) { }) .help('h') .alias({ - h: 'help' + h: 'help', + n: 'name', + c: 'channel', + p: 'prefix' }) .command( - 'env [name]', + 'env ', 'Manage Python environments', yargs => { yargs @@ -92,16 +97,37 @@ export function parseCLIArgs(argv: string[]) { type: 'string', default: '' }) - .positional('name', { + .option('name', { describe: 'Environment name', type: 'string', default: '' }) - .option('path', { - describe: 'Destination path', + .option('prefix', { + describe: 'Environment location', type: 'string', default: '' }) + .option('source', { + describe: 'Environment / package source', + type: 'string', + default: '' + }) + .option('source-type', { + describe: 'Environment / package source type', + choices: [ + 'registry', + 'bundle', + 'conda-pack', + 'conda-lock-file', + 'conda-env-file' + ], + default: 'registry' + }) + .option('channel', { + describe: 'conda package channels', + type: 'array', + default: [] + }) .option('force', { describe: 'Force the action', type: 'boolean', @@ -130,7 +156,7 @@ export function parseCLIArgs(argv: string[]) { await handleEnvListCommand(argv); break; case 'install': - await handleEnvInstallCommand(argv); + console.log('Not implemented yet!'); break; case 'activate': await handleEnvActivateCommand(argv); @@ -284,18 +310,14 @@ export function addUserSetEnvironment(envPath: string, isConda: boolean) { } } -export async function handleEnvInstallCommand(argv: any) { - let installPath: string; - if (argv.name) { - installPath = path.join(getPythonEnvsDirectory(), argv.name); - } else if (argv.path) { - installPath = argv.path; - } else { - installPath = getBundledPythonEnvPath(); - } +export async function handleInstallCondaPackEnvironment( + condaPackPath: string, + installPath: string, + forceOverwrite: boolean +) { console.log(`Installing to "${installPath}"`); - await installBundledEnvironment(installPath, { + await installCondaPackEnvironment(condaPackPath, installPath, { onInstallStatus: (status, message) => { switch (status) { case EnvironmentInstallStatus.RemovingExistingInstallation: @@ -313,7 +335,7 @@ export async function handleEnvInstallCommand(argv: any) { console.error(`Failed to install.`, message); break; case EnvironmentInstallStatus.Success: - if (argv.name || argv.path) { + if (installPath !== getBundledPythonEnvPath()) { addUserSetEnvironment(installPath, true); } console.log('Installation succeeded.'); @@ -321,13 +343,37 @@ export async function handleEnvInstallCommand(argv: any) { } }, get forceOverwrite() { - return argv.force; + return forceOverwrite; } }).catch(reason => { // }); } +async function installAdditionalCondaPackagesToEnv( + envPath: string, + packageList: string[], + channelList?: string[], + callbacks?: ICommandRunCallbacks +) { + const baseCondaEnvPath = condaEnvPathForCondaExePath(appData.condaPath); + const condaBaseEnvExists = isBaseCondaEnv(baseCondaEnvPath); + + if (!condaBaseEnvExists) { + throw { + message: `Base conda path not found "${baseCondaEnvPath}".` + }; + } + + const packages = packageList.join(); + const condaChannels = + channelList?.length > 0 ? channelList : getCondaChannels(); + const channels = condaChannels.map(channel => `-c ${channel}`).join(' '); + const installCommand = `conda install -y ${channels} -p ${envPath} ${packages}`; + console.log(`Installing additional packages: "${packages}"`); + await runCommandInEnvironment(baseCondaEnvPath, installCommand, callbacks); +} + export async function handleEnvActivateCommand(argv: any) { let envPath: string; if (argv.name) { @@ -357,23 +403,84 @@ export async function handleEnvUpdateRegistryCommand(argv: any) { appData.save(); } +export interface ICreatePythonEnvironmentOptions { + envPath: string; + envType: string; + sourceFilePath?: string; + sourceType?: + | 'registry' + | 'bundle' + | 'conda-pack' + | 'conda-lock-file' + | 'conda-env-file'; + packageList?: string[]; + condaChannels?: string[]; + callbacks?: ICommandRunCallbacks; +} + export async function createPythonEnvironment( - envPath: string, - envType: string, - packages: string, - callbacks?: ICommandRunCallbacks + options: ICreatePythonEnvironmentOptions ) { + const { + envPath, + envType, + packageList, + callbacks, + sourceFilePath, + sourceType + } = options; const isConda = envType === 'conda'; - const condaEnvPath = condaEnvPathForCondaExePath(appData.condaPath); - const condaBaseEnvExists = isBaseCondaEnv(condaEnvPath); + const baseCondaEnvPath = condaEnvPathForCondaExePath(appData.condaPath); + const condaBaseEnvExists = isBaseCondaEnv(baseCondaEnvPath); + const packages = packageList ? packageList.join(' ') : ''; if (isConda) { - const createCommand = `conda create -y -c conda-forge -p ${envPath} ${packages}`; - await runCommandInEnvironment(condaEnvPath, createCommand, callbacks); + if (!condaBaseEnvExists) { + throw { + message: + 'Failed to create Python environment. Base conda environment not found.' + }; + } + + const condaChannels = + options.condaChannels?.length > 0 + ? options.condaChannels + : getCondaChannels(); + const channels = condaChannels.map(channel => `-c ${channel}`).join(' '); + if (sourceType === 'conda-lock-file') { + const createCommand = `conda-lock install -p ${envPath} ${sourceFilePath}`; + await runCommandInEnvironment(baseCondaEnvPath, createCommand, callbacks); + + if (packages) { + const installCommand = `conda install -y ${channels} -p ${envPath} ${packages}`; + console.log(`Installing additional packages: "${packages}"`); + await runCommandInEnvironment( + baseCondaEnvPath, + installCommand, + callbacks + ); + } + } else if (sourceType === 'conda-env-file') { + const createCommand = `conda env create -p ${envPath} -f ${sourceFilePath} -y`; + await runCommandInEnvironment(baseCondaEnvPath, createCommand, callbacks); + + if (packages) { + const installCommand = `conda install -y ${channels} -p ${envPath} ${packages}`; + console.log(`Installing additional packages: "${packages}"`); + await runCommandInEnvironment( + baseCondaEnvPath, + installCommand, + callbacks + ); + } + } else { + const createCommand = `conda create -p ${envPath} ${packages} ${channels} -y`; + await runCommandInEnvironment(baseCondaEnvPath, createCommand, callbacks); + } } else { if (condaBaseEnvExists) { const createCommand = `python -m venv create ${envPath}`; - await runCommandInEnvironment(condaEnvPath, createCommand, callbacks); + await runCommandInEnvironment(baseCondaEnvPath, createCommand, callbacks); } else if (fs.existsSync(appData.systemPythonPath)) { execFileSync(appData.systemPythonPath, ['-m', 'venv', 'create', envPath]); } else { @@ -383,7 +490,7 @@ export async function createPythonEnvironment( }; } - if (packages.trim() !== '') { + if (packages) { const installCommand = `python -m pip install ${packages}`; console.log('Installing packages...'); await runCommandInEnvironment(envPath, installCommand, callbacks); @@ -401,12 +508,38 @@ export async function createPythonEnvironment( } } +function isURL(urlString: string) { + try { + const url = new URL(urlString); + return url && (url.protocol === 'https:' || url.protocol === 'http:'); + } catch (error) { + return false; + } +} + +async function downloadToTempFile( + fetchURL: string, + fileName: string +): Promise { + const downloadPath = createTempFile(fileName, '', null); + const response = await fetch(fetchURL); + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + fs.writeFileSync(downloadPath, buffer); + + return downloadPath; +} + export async function handleEnvCreateCommand(argv: any) { let envPath: string; + let installingToBundledEnvPath = false; if (argv.name) { envPath = path.join(getPythonEnvsDirectory(), argv.name); } else if (argv.path) { envPath = argv.path; + } else { + envPath = getBundledPythonEnvPath(); + installingToBundledEnvPath = true; } if (!envPath) { @@ -431,19 +564,96 @@ export async function handleEnvCreateCommand(argv: any) { } } + // if no name or prefix path specified (jlab env create), use bundled installer + let source = installingToBundledEnvPath ? 'bundle' : argv.source; + + const { sourceType } = argv; + const isCondaPackSource = source === 'bundle' || sourceType === 'conda-pack'; + const excludeJlab = argv.excludeJlab === true; const envType = argv.envType; - const isConda = envType === 'conda'; + const isConda = + envType === 'conda' || + sourceType === 'conda-pack' || + sourceType === 'conda-lock-file' || + sourceType === 'conda-env-file'; const condaEnvPath = condaEnvPathForCondaExePath(appData.condaPath); const condaBaseEnvExists = isBaseCondaEnv(condaEnvPath); - const packageList = argv._.slice(1); - if (!excludeJlab) { + const packageList: string[] = argv._.slice(1); + // add jupyterlab package unless source is conda pack + if (!isCondaPackSource && !excludeJlab) { packageList.push('jupyterlab'); } console.log(`Creating Python environment at "${envPath}"...`); + let sourceIsTempFile = false; + let sourceFilePath = ''; + + if (isCondaPackSource) { + if (source === 'bundle') { + sourceFilePath = getBundledEnvInstallerPath(); + } else if (sourceType === 'conda-pack') { + if (isURL(source)) { + try { + sourceFilePath = await downloadToTempFile(source, 'pack.tar.gz'); + sourceIsTempFile = true; + } catch (error) { + console.error(error); + } + } else { + if (fs.existsSync(source) && fs.statSync(source).isFile()) { + sourceFilePath = source; + } + } + } + + if (sourceFilePath) { + await handleInstallCondaPackEnvironment( + sourceFilePath, + envPath, + argv.force + ); + if (sourceIsTempFile) { + fs.unlinkSync(sourceFilePath); + } + + if (packageList.length > 0) { + await installAdditionalCondaPackagesToEnv( + envPath, + packageList, + argv.channel + ); + } + } + + return; + } + + if (sourceType === 'conda-lock-file' || sourceType === 'conda-env-file') { + if (isURL(source)) { + try { + sourceFilePath = await downloadToTempFile( + source, + sourceType === 'conda-lock-file' ? 'env.lock' : 'env.yml' + ); + sourceIsTempFile = true; + } catch (error) { + console.error(error); + } + } else { + if (fs.existsSync(source) && fs.statSync(source).isFile()) { + sourceFilePath = source; + } + } + + if (!sourceFilePath) { + console.error(`Invalid env source "${source}"!`); + return; + } + } + if (isConda && !condaBaseEnvExists) { console.error( 'conda base environment not found. You can set using jlab --set-base-conda-env-path command.' @@ -454,14 +664,21 @@ export async function handleEnvCreateCommand(argv: any) { const createCondaEnv = isConda || (envType === 'auto' && condaBaseEnvExists); try { - await createPythonEnvironment( + await createPythonEnvironment({ envPath, - createCondaEnv ? 'conda' : 'venv', - packageList.join(' ') - ); + envType: createCondaEnv ? 'conda' : 'venv', + sourceFilePath: sourceFilePath, + sourceType: sourceType, + packageList, + condaChannels: argv.channel + }); } catch (error) { console.error(error); } + + if (sourceIsTempFile) { + fs.unlinkSync(sourceFilePath); + } } export async function handleEnvSetCondaPathCommand(argv: any) { diff --git a/src/main/config/settings.ts b/src/main/config/settings.ts index e43a37be..8045f2ea 100644 --- a/src/main/config/settings.ts +++ b/src/main/config/settings.ts @@ -59,7 +59,8 @@ export enum SettingType { condaPath = 'condaPath', systemPythonPath = 'systemPythonPath', - pythonEnvsPath = 'pythonEnvsPath' + pythonEnvsPath = 'pythonEnvsPath', + condaChannels = 'condaChannels' } export const serverLaunchArgsFixed = [ @@ -149,7 +150,8 @@ export class UserSettings { condaPath: new Setting(''), systemPythonPath: new Setting(''), - pythonEnvsPath: new Setting('') + pythonEnvsPath: new Setting(''), + condaChannels: new Setting(['conda-forge']) }; if (readSettings) { diff --git a/src/main/env.ts b/src/main/env.ts index 30874390..48892b66 100644 --- a/src/main/env.ts +++ b/src/main/env.ts @@ -89,6 +89,15 @@ export function getCondaPath() { } } +export function getCondaChannels(): string[] { + let condaChannels = userSettings.getValue(SettingType.condaChannels); + if (condaChannels && Array.isArray(condaChannels)) { + return condaChannels; + } + + return ['conda-forge']; +} + export function getSystemPythonPath() { let pythonPath = userSettings.getValue(SettingType.systemPythonPath); if (pythonPath && fs.existsSync(pythonPath)) { @@ -221,7 +230,7 @@ export function validateNewPythonEnvironmentName( if (name.trim() === '') { message = 'Name cannot be empty'; - } else if (!name.match(/^(\w+\.?)*\w+$/)) { + } else if (!name.match(/^[a-zA-Z0-9-_]+$/)) { message = 'Name can only have letters, numbers, - and _'; } else if (fs.existsSync(path.join(envsDir, name))) { message = 'An environment with this name / directory already exists'; @@ -364,6 +373,25 @@ export async function validateCondaPath( }); } +export function validateCondaChannels( + condaChannels: string +): IFormInputValidationResponse { + let message = ''; + let valid = false; + + if (condaChannels.trim() === '') { + valid = true; + } else if (!condaChannels.match(/^[a-zA-Z0-9-_ ]+$/)) { + message = 'Channel name can only have letters, numbers, - and _'; + } else { + valid = true; + } + return { + valid, + message + }; +} + export async function validateSystemPythonPath( pythonPath: string ): Promise { diff --git a/src/main/eventtypes.ts b/src/main/eventtypes.ts index ff500def..7c1d251d 100644 --- a/src/main/eventtypes.ts +++ b/src/main/eventtypes.ts @@ -74,6 +74,8 @@ export enum EventTypeMain { SetPythonEnvironmentInstallDirectory = 'set-python-envs-directory', ValidateCondaPath = 'validate-conda-path', SetCondaPath = 'set-conda-path', + ValidateCondaChannels = 'validate-conda-channels', + SetCondaChannels = 'set-conda-channels', ValidateSystemPythonPath = 'validate-system-python-path', SetSystemPythonPath = 'set-system-python-path', CopySessionInfoToClipboard = 'copy-session-info-to-clipboard', diff --git a/src/main/pythonenvdialog/preload.ts b/src/main/pythonenvdialog/preload.ts index d0083eb2..0546ea5f 100644 --- a/src/main/pythonenvdialog/preload.ts +++ b/src/main/pythonenvdialog/preload.ts @@ -128,6 +128,15 @@ contextBridge.exposeInMainWorld('electronAPI', { setCondaPath: (condaPath: string) => { return ipcRenderer.send(EventTypeMain.SetCondaPath, condaPath); }, + validateCondaChannels: (condaChannels: string) => { + return ipcRenderer.invoke( + EventTypeMain.ValidateCondaChannels, + condaChannels + ); + }, + setCondaChannels: (condaChannels: string) => { + return ipcRenderer.send(EventTypeMain.SetCondaChannels, condaChannels); + }, validateSystemPythonPath: (pythonPath: string) => { return ipcRenderer.invoke( EventTypeMain.ValidateSystemPythonPath, diff --git a/src/main/pythonenvdialog/pythonenvdialog.ts b/src/main/pythonenvdialog/pythonenvdialog.ts index bb81f31f..3f2dbb3e 100644 --- a/src/main/pythonenvdialog/pythonenvdialog.ts +++ b/src/main/pythonenvdialog/pythonenvdialog.ts @@ -28,6 +28,7 @@ import { EventTypeMain, EventTypeRenderer } from '../eventtypes'; import { JupyterApplication } from '../app'; import { condaEnvPathForCondaExePath, + getCondaChannels, getCondaPath, getNextPythonEnvName, getPythonEnvsDirectory, @@ -41,7 +42,7 @@ export class ManagePythonEnvironmentDialog { isDarkTheme: options.isDarkTheme, title: 'Manage Python environments', width: 800, - height: 500, + height: 600, preload: path.join(__dirname, './preload.js') }); @@ -101,6 +102,7 @@ export class ManagePythonEnvironmentDialog { const pythonEnvName = getNextPythonEnvName(); const pythonEnvInstallPath = getPythonEnvsDirectory(); const condaPath = getCondaPath() || ''; + const condaChannels = getCondaChannels().join(' '); const systemPythonPath = getSystemPythonPath() || ''; const activateRelPath = process.platform === 'win32' @@ -297,6 +299,10 @@ export class ManagePythonEnvironmentDialog { } .setting-section.env-list-section { overflow-y: auto; + margin-bottom: 10px; + } + .setting-section.conda-channels-section { + width: 50%; } jp-tab-panel .setting-section:last-child { border-bottom: none; @@ -593,6 +599,19 @@ export class ManagePythonEnvironmentDialog {
+
+
+ conda channels
${infoIconSrc}
+
+
+
+ +
${checkIconSrc}
${xMarkCircleIconSrc}
+
+
+
+
+
Python path to use when creating venv environments
${infoIconSrc}
@@ -653,6 +672,7 @@ export class ManagePythonEnvironmentDialog { const pythonEnvInstallDirectoryInput = document.getElementById('python-env-install-directory'); const condaPathInput = document.getElementById('conda-path'); + const condaChannelsInput = document.getElementById('conda-channels'); const systemPythonPathInput = document.getElementById('system-python-path'); const categoryTabs = document.getElementById('category-tabs'); @@ -660,6 +680,7 @@ export class ManagePythonEnvironmentDialog { let installingJupyterLabServerEnv = false; let selectingCustomJupyterLabServerPython = false; let condaPath = <%- JSON.stringify(condaPath) %>; + let condaChannels = '<%- condaChannels %>'; let systemPythonPath = <%- JSON.stringify(systemPythonPath) %>; let envs = <%- JSON.stringify(envs) %>; @@ -671,6 +692,7 @@ export class ManagePythonEnvironmentDialog { let customPythonPathInputValidationTimer = -1; let envsDirInputValidationTimer = -1; let condaPathInputValidationTimer = -1; + let condaChannelsInputValidationTimer = -1; let systemPythonPathInputValidationTimer = -1; function handleEnvMenuClick(el) { @@ -916,7 +938,7 @@ export class ManagePythonEnvironmentDialog { function updateCreateCommandPreview() { const isConda = envTypeCondaRadio.checked; if (isConda) { - createCommandPreview.value = \`conda create -p \$\{getEnvInstallPath()\} -c conda-forge \$\{getPackageList()\}\`; + createCommandPreview.value = \`conda create -p \$\{getEnvInstallPath()\} \$\{getCondaChannels()\}\ \$\{getPackageList()\}\`; } else { const envPath = getEnvInstallPath(); createCommandPreview.value = \`python -m venv create \$\{envPath\}\n\$\{envPath\}\$\{pathSeparator\}${activateRelPath}\npython -m pip install \$\{getPackageList()\}\`; @@ -1070,6 +1092,13 @@ export class ManagePythonEnvironmentDialog { return \`\$\{pythonEnvInstallPath + pathSeparator + newEnvNameInput.value\}\`; } + function getCondaChannels() { + if (condaChannels.trim() === '') { + return ''; + } + return condaChannels.split(' ').map(channel => \`-c \$\{channel\}\`).join(' '); + } + function handleCustomPythonPathChange() { validateAndUpdateCustomPythonPath(); } @@ -1088,6 +1117,10 @@ export class ManagePythonEnvironmentDialog { validateAndUpdateCondaPath(); } + function handleCondaChannelsInputChange() { + validateAndUpdateCondaChannels(); + } + function handleSystemPythonPathInputChange() { validateAndUpdateSystemPythonPath(); } @@ -1174,6 +1207,19 @@ export class ManagePythonEnvironmentDialog { }, debounceWait); } + function validateAndUpdateCondaChannels() { + clearTimeout(condaChannelsInputValidationTimer); + condaChannelsInputValidationTimer = setTimeout(async () => { + clearInputValidStatus(condaChannelsInput); + const response = await window.electronAPI.validateCondaChannels(condaChannelsInput.value); + showInputValidStatus(condaChannelsInput, response.valid, response.message); + if (response.valid) { + condaChannels = condaChannelsInput.value; + window.electronAPI.setCondaChannels(condaChannels); + } + }, debounceWait); + } + function validateAndUpdateSystemPythonPath() { clearTimeout(systemPythonPathInputValidationTimer); systemPythonPathInputValidationTimer = setTimeout(async () => { @@ -1217,6 +1263,8 @@ export class ManagePythonEnvironmentDialog { envTypeCondaRadio.disabled = false; envTypeVenvRadio.disabled = false; } + + updateCreateCommandPreview(); } document.addEventListener("DOMContentLoaded", () => { @@ -1250,6 +1298,7 @@ export class ManagePythonEnvironmentDialog { handlePythonEnvsDirInputChange(); handleCustomPythonPathChange(); handleCondaPathInputChange(); + handleCondaChannelsInputChange(); handleSystemPythonPathInputChange(); }, 1000); }); @@ -1264,6 +1313,7 @@ export class ManagePythonEnvironmentDialog { pythonEnvName, pythonEnvInstallPath, condaPath, + condaChannels, systemPythonPath }); } diff --git a/src/main/pythonenvselectpopup/pythonenvselectpopup.ts b/src/main/pythonenvselectpopup/pythonenvselectpopup.ts index fadec30a..9365ef80 100644 --- a/src/main/pythonenvselectpopup/pythonenvselectpopup.ts +++ b/src/main/pythonenvselectpopup/pythonenvselectpopup.ts @@ -55,7 +55,7 @@ export class PythonEnvironmentSelectPopup { gap: 10px; color: var(--neutral-foreground-hint); } - #restart-title { + #popup-title { flex-grow: 1; margin-left: 5px; height: 30px; @@ -134,8 +134,8 @@ export class PythonEnvironmentSelectPopup {