├── .gitignore ├── Jim.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── xcshareddata │ └── xcschemes │ │ └── Jim.xcscheme └── xcuserdata │ └── seem.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── Jim ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── Cell │ ├── CellView.swift │ ├── CellViewModel.swift │ ├── MarkdownStyler.swift │ ├── MinimalTextView.swift │ ├── OutputStackView.swift │ ├── OutputTextView.swift │ ├── RichTextView.swift │ └── RunButton.swift ├── Jim.entitlements ├── Jupyter │ ├── Cell.swift │ ├── Content.swift │ ├── JupyterService.swift │ ├── Message.swift │ ├── Notebook.swift │ ├── Output.swift │ └── Session.swift ├── Main │ ├── SidebarViewController.swift │ └── WindowController.swift ├── Notebook │ ├── NotebookTableRowView.swift │ ├── NotebookTableView.swift │ ├── NotebookViewController.swift │ └── NotebookViewModel.swift ├── Source │ ├── SourceScroller.swift │ ├── SourceTextView.swift │ └── SourceView.swift ├── Theme.swift └── Utils │ ├── Base64Image.swift │ ├── Debug.swift │ └── StringOrArray.swift ├── LICENSE ├── README.md └── Whats New └── 2023-03-29-whats-new.ipynb /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | 70 | .DS_Store 71 | 72 | node_modules 73 | dist/ 74 | 75 | .ipynb_checkpoints/ 76 | 77 | -------------------------------------------------------------------------------- /Jim.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 841C6ECF29A6268D008B1FE5 /* CellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841C6ECE29A6268D008B1FE5 /* CellView.swift */; }; 11 | 842CBCB029CEC02600C0DE4C /* Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842CBCAF29CEC02600C0DE4C /* Debug.swift */; }; 12 | 842CBCB229CED64D00C0DE4C /* SourceTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842CBCB129CED64D00C0DE4C /* SourceTextView.swift */; }; 13 | 842CBCC029D03B8A00C0DE4C /* SourceScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842CBCBF29D03B8A00C0DE4C /* SourceScroller.swift */; }; 14 | 842CBCC229D059F000C0DE4C /* RichTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842CBCC129D059F000C0DE4C /* RichTextView.swift */; }; 15 | 842CBCC529D16F5500C0DE4C /* Down in Frameworks */ = {isa = PBXBuildFile; productRef = 842CBCC429D16F5500C0DE4C /* Down */; }; 16 | 8437F0F829A4F0F500D027B7 /* WindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8437F0F729A4F0F500D027B7 /* WindowController.swift */; }; 17 | 8456BD40299D0264001E3EDA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8456BD3F299D0264001E3EDA /* AppDelegate.swift */; }; 18 | 8456BD44299D0268001E3EDA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8456BD43299D0268001E3EDA /* Assets.xcassets */; }; 19 | 8456BD47299D0268001E3EDA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8456BD45299D0268001E3EDA /* Main.storyboard */; }; 20 | 8456BD4F299D05A5001E3EDA /* SidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8456BD4E299D05A5001E3EDA /* SidebarViewController.swift */; }; 21 | 8456BD51299D0F5E001E3EDA /* NotebookViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8456BD50299D0F5E001E3EDA /* NotebookViewController.swift */; }; 22 | 845D8A5C29CEB8A9007A624B /* MinimalTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845D8A5B29CEB8A9007A624B /* MinimalTextView.swift */; }; 23 | 8478D3F329C9807F0028A8E1 /* OutputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8478D3F229C9807F0028A8E1 /* OutputTextView.swift */; }; 24 | 8478D3F529C9809B0028A8E1 /* OutputStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8478D3F429C9809B0028A8E1 /* OutputStackView.swift */; }; 25 | 8478D3FB29C99AC20028A8E1 /* CellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8478D3FA29C99AC20028A8E1 /* CellViewModel.swift */; }; 26 | 8478D3FD29C99C150028A8E1 /* NotebookViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8478D3FC29C99C150028A8E1 /* NotebookViewModel.swift */; }; 27 | 84C838E6299D6E2900BA3F8F /* AnyCodable in Frameworks */ = {isa = PBXBuildFile; productRef = 84C838E5299D6E2900BA3F8F /* AnyCodable */; }; 28 | 84C838EA299D6E4600BA3F8F /* StringOrArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C838E8299D6E4600BA3F8F /* StringOrArray.swift */; }; 29 | 84C838EB299D6E4600BA3F8F /* Base64Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C838E9299D6E4600BA3F8F /* Base64Image.swift */; }; 30 | 84CCB4E429C4894900EC17B3 /* NotebookTableRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCB4E329C4894900EC17B3 /* NotebookTableRowView.swift */; }; 31 | 84CE599129A762990094FE3E /* SourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE597429A762990094FE3E /* SourceView.swift */; }; 32 | 84CE599829A762990094FE3E /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE597B29A762990094FE3E /* Theme.swift */; }; 33 | 84D3E83029D46F9A007E6B72 /* MarkdownStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D3E82F29D46F9A007E6B72 /* MarkdownStyler.swift */; }; 34 | 84ED48F229B9C0A70069AFD8 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ED48EC29B9C0A70069AFD8 /* Session.swift */; }; 35 | 84ED48F329B9C0A70069AFD8 /* JupyterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ED48ED29B9C0A70069AFD8 /* JupyterService.swift */; }; 36 | 84ED48F429B9C0A70069AFD8 /* Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ED48EE29B9C0A70069AFD8 /* Cell.swift */; }; 37 | 84ED48F529B9C0A70069AFD8 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ED48EF29B9C0A70069AFD8 /* Message.swift */; }; 38 | 84ED48F629B9C0A70069AFD8 /* Notebook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ED48F029B9C0A70069AFD8 /* Notebook.swift */; }; 39 | 84ED48F729B9C0A70069AFD8 /* Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ED48F129B9C0A70069AFD8 /* Content.swift */; }; 40 | 84ED48FB29BF2EFA0069AFD8 /* Output.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ED48FA29BF2EFA0069AFD8 /* Output.swift */; }; 41 | 84ED491929BF50720069AFD8 /* NotebookTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ED491829BF50720069AFD8 /* NotebookTableView.swift */; }; 42 | 84ED491B29BF517C0069AFD8 /* RunButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ED491A29BF517C0069AFD8 /* RunButton.swift */; }; 43 | /* End PBXBuildFile section */ 44 | 45 | /* Begin PBXFileReference section */ 46 | 841C6ECE29A6268D008B1FE5 /* CellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellView.swift; sourceTree = ""; }; 47 | 842CBCAF29CEC02600C0DE4C /* Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debug.swift; sourceTree = ""; }; 48 | 842CBCB129CED64D00C0DE4C /* SourceTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceTextView.swift; sourceTree = ""; }; 49 | 842CBCBF29D03B8A00C0DE4C /* SourceScroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceScroller.swift; sourceTree = ""; }; 50 | 842CBCC129D059F000C0DE4C /* RichTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichTextView.swift; sourceTree = ""; }; 51 | 8437F0F729A4F0F500D027B7 /* WindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowController.swift; sourceTree = ""; }; 52 | 8456BD3C299D0264001E3EDA /* Jim.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Jim.app; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | 8456BD3F299D0264001E3EDA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 54 | 8456BD43299D0268001E3EDA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 55 | 8456BD46299D0268001E3EDA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 56 | 8456BD48299D0268001E3EDA /* Jim.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Jim.entitlements; sourceTree = ""; }; 57 | 8456BD4E299D05A5001E3EDA /* SidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewController.swift; sourceTree = ""; }; 58 | 8456BD50299D0F5E001E3EDA /* NotebookViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotebookViewController.swift; sourceTree = ""; }; 59 | 845D8A5B29CEB8A9007A624B /* MinimalTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinimalTextView.swift; sourceTree = ""; }; 60 | 8478D3F229C9807F0028A8E1 /* OutputTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputTextView.swift; sourceTree = ""; }; 61 | 8478D3F429C9809B0028A8E1 /* OutputStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputStackView.swift; sourceTree = ""; }; 62 | 8478D3FA29C99AC20028A8E1 /* CellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellViewModel.swift; sourceTree = ""; }; 63 | 8478D3FC29C99C150028A8E1 /* NotebookViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotebookViewModel.swift; sourceTree = ""; }; 64 | 84C838E8299D6E4600BA3F8F /* StringOrArray.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringOrArray.swift; sourceTree = ""; }; 65 | 84C838E9299D6E4600BA3F8F /* Base64Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Base64Image.swift; sourceTree = ""; }; 66 | 84CCB4E329C4894900EC17B3 /* NotebookTableRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotebookTableRowView.swift; sourceTree = ""; }; 67 | 84CE597429A762990094FE3E /* SourceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SourceView.swift; sourceTree = ""; }; 68 | 84CE597B29A762990094FE3E /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; 69 | 84D3E82F29D46F9A007E6B72 /* MarkdownStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownStyler.swift; sourceTree = ""; }; 70 | 84ED48EC29B9C0A70069AFD8 /* Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Session.swift; sourceTree = ""; }; 71 | 84ED48ED29B9C0A70069AFD8 /* JupyterService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JupyterService.swift; sourceTree = ""; }; 72 | 84ED48EE29B9C0A70069AFD8 /* Cell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cell.swift; sourceTree = ""; }; 73 | 84ED48EF29B9C0A70069AFD8 /* Message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; 74 | 84ED48F029B9C0A70069AFD8 /* Notebook.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notebook.swift; sourceTree = ""; }; 75 | 84ED48F129B9C0A70069AFD8 /* Content.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Content.swift; sourceTree = ""; }; 76 | 84ED48FA29BF2EFA0069AFD8 /* Output.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Output.swift; sourceTree = ""; }; 77 | 84ED491829BF50720069AFD8 /* NotebookTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotebookTableView.swift; sourceTree = ""; }; 78 | 84ED491A29BF517C0069AFD8 /* RunButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunButton.swift; sourceTree = ""; }; 79 | /* End PBXFileReference section */ 80 | 81 | /* Begin PBXFrameworksBuildPhase section */ 82 | 8456BD39299D0264001E3EDA /* Frameworks */ = { 83 | isa = PBXFrameworksBuildPhase; 84 | buildActionMask = 2147483647; 85 | files = ( 86 | 842CBCC529D16F5500C0DE4C /* Down in Frameworks */, 87 | 84C838E6299D6E2900BA3F8F /* AnyCodable in Frameworks */, 88 | ); 89 | runOnlyForDeploymentPostprocessing = 0; 90 | }; 91 | /* End PBXFrameworksBuildPhase section */ 92 | 93 | /* Begin PBXGroup section */ 94 | 8456BD33299D0264001E3EDA = { 95 | isa = PBXGroup; 96 | children = ( 97 | 8456BD3E299D0264001E3EDA /* Jim */, 98 | 8456BD3D299D0264001E3EDA /* Products */, 99 | ); 100 | sourceTree = ""; 101 | }; 102 | 8456BD3D299D0264001E3EDA /* Products */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 8456BD3C299D0264001E3EDA /* Jim.app */, 106 | ); 107 | name = Products; 108 | sourceTree = ""; 109 | }; 110 | 8456BD3E299D0264001E3EDA /* Jim */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 8478D3F729C981900028A8E1 /* Main */, 114 | 8478D3F829C981F50028A8E1 /* Notebook */, 115 | 8478D3F929C9821A0028A8E1 /* Cell */, 116 | 84CE595F29A762990094FE3E /* Source */, 117 | 84ED48EB29B9C0A70069AFD8 /* Jupyter */, 118 | 84C838E7299D6E4600BA3F8F /* Utils */, 119 | 8456BD3F299D0264001E3EDA /* AppDelegate.swift */, 120 | 84CE597B29A762990094FE3E /* Theme.swift */, 121 | 8456BD45299D0268001E3EDA /* Main.storyboard */, 122 | 8456BD43299D0268001E3EDA /* Assets.xcassets */, 123 | 8456BD48299D0268001E3EDA /* Jim.entitlements */, 124 | ); 125 | path = Jim; 126 | sourceTree = ""; 127 | }; 128 | 8478D3F729C981900028A8E1 /* Main */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | 8437F0F729A4F0F500D027B7 /* WindowController.swift */, 132 | 8456BD4E299D05A5001E3EDA /* SidebarViewController.swift */, 133 | ); 134 | path = Main; 135 | sourceTree = ""; 136 | }; 137 | 8478D3F829C981F50028A8E1 /* Notebook */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | 8456BD50299D0F5E001E3EDA /* NotebookViewController.swift */, 141 | 8478D3FC29C99C150028A8E1 /* NotebookViewModel.swift */, 142 | 84ED491829BF50720069AFD8 /* NotebookTableView.swift */, 143 | 84CCB4E329C4894900EC17B3 /* NotebookTableRowView.swift */, 144 | ); 145 | path = Notebook; 146 | sourceTree = ""; 147 | }; 148 | 8478D3F929C9821A0028A8E1 /* Cell */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 841C6ECE29A6268D008B1FE5 /* CellView.swift */, 152 | 8478D3FA29C99AC20028A8E1 /* CellViewModel.swift */, 153 | 842CBCC129D059F000C0DE4C /* RichTextView.swift */, 154 | 8478D3F429C9809B0028A8E1 /* OutputStackView.swift */, 155 | 8478D3F229C9807F0028A8E1 /* OutputTextView.swift */, 156 | 84ED491A29BF517C0069AFD8 /* RunButton.swift */, 157 | 845D8A5B29CEB8A9007A624B /* MinimalTextView.swift */, 158 | 84D3E82F29D46F9A007E6B72 /* MarkdownStyler.swift */, 159 | ); 160 | path = Cell; 161 | sourceTree = ""; 162 | }; 163 | 84C838E7299D6E4600BA3F8F /* Utils */ = { 164 | isa = PBXGroup; 165 | children = ( 166 | 84C838E8299D6E4600BA3F8F /* StringOrArray.swift */, 167 | 84C838E9299D6E4600BA3F8F /* Base64Image.swift */, 168 | 842CBCAF29CEC02600C0DE4C /* Debug.swift */, 169 | ); 170 | path = Utils; 171 | sourceTree = ""; 172 | }; 173 | 84CE595F29A762990094FE3E /* Source */ = { 174 | isa = PBXGroup; 175 | children = ( 176 | 84CE597429A762990094FE3E /* SourceView.swift */, 177 | 842CBCB129CED64D00C0DE4C /* SourceTextView.swift */, 178 | 842CBCBF29D03B8A00C0DE4C /* SourceScroller.swift */, 179 | ); 180 | path = Source; 181 | sourceTree = ""; 182 | }; 183 | 84ED48EB29B9C0A70069AFD8 /* Jupyter */ = { 184 | isa = PBXGroup; 185 | children = ( 186 | 84ED48EC29B9C0A70069AFD8 /* Session.swift */, 187 | 84ED48ED29B9C0A70069AFD8 /* JupyterService.swift */, 188 | 84ED48EE29B9C0A70069AFD8 /* Cell.swift */, 189 | 84ED48EF29B9C0A70069AFD8 /* Message.swift */, 190 | 84ED48F029B9C0A70069AFD8 /* Notebook.swift */, 191 | 84ED48F129B9C0A70069AFD8 /* Content.swift */, 192 | 84ED48FA29BF2EFA0069AFD8 /* Output.swift */, 193 | ); 194 | path = Jupyter; 195 | sourceTree = ""; 196 | }; 197 | /* End PBXGroup section */ 198 | 199 | /* Begin PBXNativeTarget section */ 200 | 8456BD3B299D0264001E3EDA /* Jim */ = { 201 | isa = PBXNativeTarget; 202 | buildConfigurationList = 8456BD4B299D0268001E3EDA /* Build configuration list for PBXNativeTarget "Jim" */; 203 | buildPhases = ( 204 | 8456BD38299D0264001E3EDA /* Sources */, 205 | 8456BD39299D0264001E3EDA /* Frameworks */, 206 | 8456BD3A299D0264001E3EDA /* Resources */, 207 | ); 208 | buildRules = ( 209 | ); 210 | dependencies = ( 211 | ); 212 | name = Jim; 213 | packageProductDependencies = ( 214 | 84C838E5299D6E2900BA3F8F /* AnyCodable */, 215 | 842CBCC429D16F5500C0DE4C /* Down */, 216 | ); 217 | productName = Jim; 218 | productReference = 8456BD3C299D0264001E3EDA /* Jim.app */; 219 | productType = "com.apple.product-type.application"; 220 | }; 221 | /* End PBXNativeTarget section */ 222 | 223 | /* Begin PBXProject section */ 224 | 8456BD34299D0264001E3EDA /* Project object */ = { 225 | isa = PBXProject; 226 | attributes = { 227 | BuildIndependentTargetsInParallel = 1; 228 | LastSwiftUpdateCheck = 1420; 229 | LastUpgradeCheck = 1420; 230 | TargetAttributes = { 231 | 8456BD3B299D0264001E3EDA = { 232 | CreatedOnToolsVersion = 14.2; 233 | }; 234 | }; 235 | }; 236 | buildConfigurationList = 8456BD37299D0264001E3EDA /* Build configuration list for PBXProject "Jim" */; 237 | compatibilityVersion = "Xcode 14.0"; 238 | developmentRegion = en; 239 | hasScannedForEncodings = 0; 240 | knownRegions = ( 241 | en, 242 | Base, 243 | ); 244 | mainGroup = 8456BD33299D0264001E3EDA; 245 | packageReferences = ( 246 | 84C838E4299D6E2800BA3F8F /* XCRemoteSwiftPackageReference "AnyCodable" */, 247 | 842CBCC329D16F5500C0DE4C /* XCRemoteSwiftPackageReference "Down" */, 248 | ); 249 | productRefGroup = 8456BD3D299D0264001E3EDA /* Products */; 250 | projectDirPath = ""; 251 | projectRoot = ""; 252 | targets = ( 253 | 8456BD3B299D0264001E3EDA /* Jim */, 254 | ); 255 | }; 256 | /* End PBXProject section */ 257 | 258 | /* Begin PBXResourcesBuildPhase section */ 259 | 8456BD3A299D0264001E3EDA /* Resources */ = { 260 | isa = PBXResourcesBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | 8456BD44299D0268001E3EDA /* Assets.xcassets in Resources */, 264 | 8456BD47299D0268001E3EDA /* Main.storyboard in Resources */, 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | /* End PBXResourcesBuildPhase section */ 269 | 270 | /* Begin PBXSourcesBuildPhase section */ 271 | 8456BD38299D0264001E3EDA /* Sources */ = { 272 | isa = PBXSourcesBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | 84ED48F229B9C0A70069AFD8 /* Session.swift in Sources */, 276 | 8478D3F529C9809B0028A8E1 /* OutputStackView.swift in Sources */, 277 | 842CBCB229CED64D00C0DE4C /* SourceTextView.swift in Sources */, 278 | 84ED491929BF50720069AFD8 /* NotebookTableView.swift in Sources */, 279 | 841C6ECF29A6268D008B1FE5 /* CellView.swift in Sources */, 280 | 8456BD4F299D05A5001E3EDA /* SidebarViewController.swift in Sources */, 281 | 84ED48F329B9C0A70069AFD8 /* JupyterService.swift in Sources */, 282 | 84CE599829A762990094FE3E /* Theme.swift in Sources */, 283 | 8478D3FB29C99AC20028A8E1 /* CellViewModel.swift in Sources */, 284 | 84CCB4E429C4894900EC17B3 /* NotebookTableRowView.swift in Sources */, 285 | 84D3E83029D46F9A007E6B72 /* MarkdownStyler.swift in Sources */, 286 | 84ED491B29BF517C0069AFD8 /* RunButton.swift in Sources */, 287 | 842CBCB029CEC02600C0DE4C /* Debug.swift in Sources */, 288 | 842CBCC029D03B8A00C0DE4C /* SourceScroller.swift in Sources */, 289 | 84ED48F629B9C0A70069AFD8 /* Notebook.swift in Sources */, 290 | 8456BD40299D0264001E3EDA /* AppDelegate.swift in Sources */, 291 | 84ED48FB29BF2EFA0069AFD8 /* Output.swift in Sources */, 292 | 84C838EA299D6E4600BA3F8F /* StringOrArray.swift in Sources */, 293 | 8456BD51299D0F5E001E3EDA /* NotebookViewController.swift in Sources */, 294 | 845D8A5C29CEB8A9007A624B /* MinimalTextView.swift in Sources */, 295 | 842CBCC229D059F000C0DE4C /* RichTextView.swift in Sources */, 296 | 8437F0F829A4F0F500D027B7 /* WindowController.swift in Sources */, 297 | 84ED48F729B9C0A70069AFD8 /* Content.swift in Sources */, 298 | 84ED48F529B9C0A70069AFD8 /* Message.swift in Sources */, 299 | 84C838EB299D6E4600BA3F8F /* Base64Image.swift in Sources */, 300 | 8478D3F329C9807F0028A8E1 /* OutputTextView.swift in Sources */, 301 | 84ED48F429B9C0A70069AFD8 /* Cell.swift in Sources */, 302 | 84CE599129A762990094FE3E /* SourceView.swift in Sources */, 303 | 8478D3FD29C99C150028A8E1 /* NotebookViewModel.swift in Sources */, 304 | ); 305 | runOnlyForDeploymentPostprocessing = 0; 306 | }; 307 | /* End PBXSourcesBuildPhase section */ 308 | 309 | /* Begin PBXVariantGroup section */ 310 | 8456BD45299D0268001E3EDA /* Main.storyboard */ = { 311 | isa = PBXVariantGroup; 312 | children = ( 313 | 8456BD46299D0268001E3EDA /* Base */, 314 | ); 315 | name = Main.storyboard; 316 | sourceTree = ""; 317 | }; 318 | /* End PBXVariantGroup section */ 319 | 320 | /* Begin XCBuildConfiguration section */ 321 | 8456BD49299D0268001E3EDA /* Debug */ = { 322 | isa = XCBuildConfiguration; 323 | buildSettings = { 324 | ALWAYS_SEARCH_USER_PATHS = NO; 325 | CLANG_ANALYZER_NONNULL = YES; 326 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 327 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 328 | CLANG_ENABLE_MODULES = YES; 329 | CLANG_ENABLE_OBJC_ARC = YES; 330 | CLANG_ENABLE_OBJC_WEAK = YES; 331 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 332 | CLANG_WARN_BOOL_CONVERSION = YES; 333 | CLANG_WARN_COMMA = YES; 334 | CLANG_WARN_CONSTANT_CONVERSION = YES; 335 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 336 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 337 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 338 | CLANG_WARN_EMPTY_BODY = YES; 339 | CLANG_WARN_ENUM_CONVERSION = YES; 340 | CLANG_WARN_INFINITE_RECURSION = YES; 341 | CLANG_WARN_INT_CONVERSION = YES; 342 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 343 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 344 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 345 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 346 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 347 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 348 | CLANG_WARN_STRICT_PROTOTYPES = YES; 349 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 350 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 351 | CLANG_WARN_UNREACHABLE_CODE = YES; 352 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 353 | COPY_PHASE_STRIP = NO; 354 | DEBUG_INFORMATION_FORMAT = dwarf; 355 | ENABLE_STRICT_OBJC_MSGSEND = YES; 356 | ENABLE_TESTABILITY = YES; 357 | GCC_C_LANGUAGE_STANDARD = gnu11; 358 | GCC_DYNAMIC_NO_PIC = NO; 359 | GCC_NO_COMMON_BLOCKS = YES; 360 | GCC_OPTIMIZATION_LEVEL = 0; 361 | GCC_PREPROCESSOR_DEFINITIONS = ( 362 | "DEBUG=1", 363 | "$(inherited)", 364 | ); 365 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 366 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 367 | GCC_WARN_UNDECLARED_SELECTOR = YES; 368 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 369 | GCC_WARN_UNUSED_FUNCTION = YES; 370 | GCC_WARN_UNUSED_VARIABLE = YES; 371 | MACOSX_DEPLOYMENT_TARGET = 13.0; 372 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 373 | MTL_FAST_MATH = YES; 374 | ONLY_ACTIVE_ARCH = YES; 375 | SDKROOT = macosx; 376 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 377 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 378 | }; 379 | name = Debug; 380 | }; 381 | 8456BD4A299D0268001E3EDA /* Release */ = { 382 | isa = XCBuildConfiguration; 383 | buildSettings = { 384 | ALWAYS_SEARCH_USER_PATHS = NO; 385 | CLANG_ANALYZER_NONNULL = YES; 386 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 387 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 388 | CLANG_ENABLE_MODULES = YES; 389 | CLANG_ENABLE_OBJC_ARC = YES; 390 | CLANG_ENABLE_OBJC_WEAK = YES; 391 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 392 | CLANG_WARN_BOOL_CONVERSION = YES; 393 | CLANG_WARN_COMMA = YES; 394 | CLANG_WARN_CONSTANT_CONVERSION = YES; 395 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 396 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 397 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 398 | CLANG_WARN_EMPTY_BODY = YES; 399 | CLANG_WARN_ENUM_CONVERSION = YES; 400 | CLANG_WARN_INFINITE_RECURSION = YES; 401 | CLANG_WARN_INT_CONVERSION = YES; 402 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 403 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 404 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 405 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 406 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 407 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 408 | CLANG_WARN_STRICT_PROTOTYPES = YES; 409 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 410 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 411 | CLANG_WARN_UNREACHABLE_CODE = YES; 412 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 413 | COPY_PHASE_STRIP = NO; 414 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 415 | ENABLE_NS_ASSERTIONS = NO; 416 | ENABLE_STRICT_OBJC_MSGSEND = YES; 417 | GCC_C_LANGUAGE_STANDARD = gnu11; 418 | GCC_NO_COMMON_BLOCKS = YES; 419 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 420 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 421 | GCC_WARN_UNDECLARED_SELECTOR = YES; 422 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 423 | GCC_WARN_UNUSED_FUNCTION = YES; 424 | GCC_WARN_UNUSED_VARIABLE = YES; 425 | MACOSX_DEPLOYMENT_TARGET = 13.0; 426 | MTL_ENABLE_DEBUG_INFO = NO; 427 | MTL_FAST_MATH = YES; 428 | SDKROOT = macosx; 429 | SWIFT_COMPILATION_MODE = wholemodule; 430 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 431 | }; 432 | name = Release; 433 | }; 434 | 8456BD4C299D0268001E3EDA /* Debug */ = { 435 | isa = XCBuildConfiguration; 436 | buildSettings = { 437 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 438 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 439 | CODE_SIGN_ENTITLEMENTS = Jim/Jim.entitlements; 440 | CODE_SIGN_STYLE = Automatic; 441 | COMBINE_HIDPI_IMAGES = YES; 442 | CURRENT_PROJECT_VERSION = 1; 443 | GENERATE_INFOPLIST_FILE = YES; 444 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 445 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 446 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 447 | LD_RUNPATH_SEARCH_PATHS = ( 448 | "$(inherited)", 449 | "@executable_path/../Frameworks", 450 | ); 451 | MARKETING_VERSION = 1.0; 452 | PRODUCT_BUNDLE_IDENTIFIER = com.wasimlorgat.Jim; 453 | PRODUCT_NAME = "$(TARGET_NAME)"; 454 | SWIFT_EMIT_LOC_STRINGS = YES; 455 | SWIFT_VERSION = 5.0; 456 | }; 457 | name = Debug; 458 | }; 459 | 8456BD4D299D0268001E3EDA /* Release */ = { 460 | isa = XCBuildConfiguration; 461 | buildSettings = { 462 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 463 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 464 | CODE_SIGN_ENTITLEMENTS = Jim/Jim.entitlements; 465 | CODE_SIGN_STYLE = Automatic; 466 | COMBINE_HIDPI_IMAGES = YES; 467 | CURRENT_PROJECT_VERSION = 1; 468 | GENERATE_INFOPLIST_FILE = YES; 469 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 470 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 471 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 472 | LD_RUNPATH_SEARCH_PATHS = ( 473 | "$(inherited)", 474 | "@executable_path/../Frameworks", 475 | ); 476 | MARKETING_VERSION = 1.0; 477 | PRODUCT_BUNDLE_IDENTIFIER = com.wasimlorgat.Jim; 478 | PRODUCT_NAME = "$(TARGET_NAME)"; 479 | SWIFT_EMIT_LOC_STRINGS = YES; 480 | SWIFT_VERSION = 5.0; 481 | }; 482 | name = Release; 483 | }; 484 | /* End XCBuildConfiguration section */ 485 | 486 | /* Begin XCConfigurationList section */ 487 | 8456BD37299D0264001E3EDA /* Build configuration list for PBXProject "Jim" */ = { 488 | isa = XCConfigurationList; 489 | buildConfigurations = ( 490 | 8456BD49299D0268001E3EDA /* Debug */, 491 | 8456BD4A299D0268001E3EDA /* Release */, 492 | ); 493 | defaultConfigurationIsVisible = 0; 494 | defaultConfigurationName = Release; 495 | }; 496 | 8456BD4B299D0268001E3EDA /* Build configuration list for PBXNativeTarget "Jim" */ = { 497 | isa = XCConfigurationList; 498 | buildConfigurations = ( 499 | 8456BD4C299D0268001E3EDA /* Debug */, 500 | 8456BD4D299D0268001E3EDA /* Release */, 501 | ); 502 | defaultConfigurationIsVisible = 0; 503 | defaultConfigurationName = Release; 504 | }; 505 | /* End XCConfigurationList section */ 506 | 507 | /* Begin XCRemoteSwiftPackageReference section */ 508 | 842CBCC329D16F5500C0DE4C /* XCRemoteSwiftPackageReference "Down" */ = { 509 | isa = XCRemoteSwiftPackageReference; 510 | repositoryURL = "https://github.com/johnxnguyen/Down"; 511 | requirement = { 512 | branch = master; 513 | kind = branch; 514 | }; 515 | }; 516 | 84C838E4299D6E2800BA3F8F /* XCRemoteSwiftPackageReference "AnyCodable" */ = { 517 | isa = XCRemoteSwiftPackageReference; 518 | repositoryURL = "https://github.com/Flight-School/AnyCodable"; 519 | requirement = { 520 | branch = master; 521 | kind = branch; 522 | }; 523 | }; 524 | /* End XCRemoteSwiftPackageReference section */ 525 | 526 | /* Begin XCSwiftPackageProductDependency section */ 527 | 842CBCC429D16F5500C0DE4C /* Down */ = { 528 | isa = XCSwiftPackageProductDependency; 529 | package = 842CBCC329D16F5500C0DE4C /* XCRemoteSwiftPackageReference "Down" */; 530 | productName = Down; 531 | }; 532 | 84C838E5299D6E2900BA3F8F /* AnyCodable */ = { 533 | isa = XCSwiftPackageProductDependency; 534 | package = 84C838E4299D6E2800BA3F8F /* XCRemoteSwiftPackageReference "AnyCodable" */; 535 | productName = AnyCodable; 536 | }; 537 | /* End XCSwiftPackageProductDependency section */ 538 | }; 539 | rootObject = 8456BD34299D0264001E3EDA /* Project object */; 540 | } 541 | -------------------------------------------------------------------------------- /Jim.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Jim.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Jim.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "anycodable", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/Flight-School/AnyCodable", 7 | "state" : { 8 | "branch" : "master", 9 | "revision" : "862808b2070cd908cb04f9aafe7de83d35f81b05" 10 | } 11 | }, 12 | { 13 | "identity" : "down", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/johnxnguyen/Down", 16 | "state" : { 17 | "branch" : "master", 18 | "revision" : "e754ab1c80920dd51a8e08290c912ac1c2ac8b58" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /Jim.xcodeproj/xcshareddata/xcschemes/Jim.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | 66 | 68 | 74 | 75 | 76 | 77 | 79 | 80 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /Jim.xcodeproj/xcuserdata/seem.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | AnyCodable (Playground) 1.xcscheme 8 | 9 | isShown 10 | 11 | orderHint 12 | 2 13 | 14 | AnyCodable (Playground) 2.xcscheme 15 | 16 | isShown 17 | 18 | orderHint 19 | 3 20 | 21 | AnyCodable (Playground).xcscheme 22 | 23 | isShown 24 | 25 | orderHint 26 | 0 27 | 28 | Jim.xcscheme_^#shared#^_ 29 | 30 | orderHint 31 | 1 32 | 33 | 34 | SuppressBuildableAutocreation 35 | 36 | 8456BD3B299D0264001E3EDA 37 | 38 | primary 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Jim/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | @main 4 | class AppDelegate: NSObject, NSApplicationDelegate { 5 | 6 | func applicationDidFinishLaunching(_ aNotification: Notification) { 7 | // Insert code here to initialize your application 8 | } 9 | 10 | func applicationWillTerminate(_ aNotification: Notification) { 11 | // Insert code here to tear down your application 12 | } 13 | 14 | func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 15 | return true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Jim/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Jim/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Jim/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Jim/Cell/CellView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Combine 3 | import Foundation 4 | 5 | class OpaqueView: NSView { 6 | override var isOpaque: Bool { true } 7 | } 8 | 9 | class CellView: NSTableCellView { 10 | let sourceView = SourceView() 11 | let richTextView = RichTextView() 12 | let outputStackView = NSStackView() 13 | var tableView: NotebookTableView! 14 | var row: Int { tableView.row(for: self) } 15 | 16 | // Caching 17 | private var reusableImageViews = [NSImageView]() 18 | private var reusableTextViews = [OutputTextView]() 19 | 20 | override var isOpaque: Bool { true } 21 | 22 | var viewModel: CellViewModel! 23 | 24 | // For switching between edit and rich text mode 25 | private var sourceViewVerticalConstraints: [NSLayoutConstraint]! 26 | private var richTextViewVerticalConstraints: [NSLayoutConstraint]! 27 | 28 | override init(frame frameRect: NSRect) { 29 | super.init(frame: frameRect) 30 | setup() 31 | } 32 | 33 | required init?(coder: NSCoder) { 34 | super.init(coder: coder) 35 | setup() 36 | } 37 | 38 | private func setup() { 39 | outputStackView.orientation = .vertical 40 | 41 | sourceView.delegate = self 42 | 43 | richTextView.customDelegate = self 44 | 45 | addSubview(sourceView) 46 | addSubview(richTextView) 47 | addSubview(outputStackView) 48 | 49 | sourceView.translatesAutoresizingMaskIntoConstraints = false 50 | richTextView.translatesAutoresizingMaskIntoConstraints = false 51 | outputStackView.translatesAutoresizingMaskIntoConstraints = false 52 | richTextView.setContentHuggingPriority(.required, for: .vertical) 53 | sourceView.setContentHuggingPriority(.required, for: .vertical) 54 | outputStackView.setHuggingPriority(.required, for: .vertical) 55 | NSLayoutConstraint.activate([ 56 | outputStackView.bottomAnchor.constraint(equalTo: bottomAnchor), 57 | outputStackView.leadingAnchor.constraint(equalTo: sourceView.leadingAnchor), 58 | outputStackView.trailingAnchor.constraint(equalTo: trailingAnchor), 59 | ]) 60 | sourceViewVerticalConstraints = [ 61 | sourceView.topAnchor.constraint(equalTo: topAnchor), 62 | outputStackView.topAnchor.constraint(equalTo: sourceView.bottomAnchor), 63 | sourceView.leadingAnchor.constraint(equalTo: leadingAnchor), 64 | sourceView.trailingAnchor.constraint(equalTo: trailingAnchor), 65 | ] 66 | richTextViewVerticalConstraints = [ 67 | richTextView.topAnchor.constraint(equalTo: topAnchor), 68 | outputStackView.topAnchor.constraint(equalTo: richTextView.bottomAnchor), 69 | richTextView.leadingAnchor.constraint(equalTo: leadingAnchor), 70 | richTextView.trailingAnchor.constraint(equalTo: trailingAnchor), 71 | ] 72 | 73 | showSourceView() 74 | } 75 | 76 | func showSourceView() { 77 | NSAnimationContext.runAnimationGroup { context in 78 | context.duration = 0 79 | context.allowsImplicitAnimation = false 80 | 81 | sourceView.isHidden = false 82 | richTextView.isHidden = true 83 | NSLayoutConstraint.deactivate(richTextViewVerticalConstraints) 84 | NSLayoutConstraint.activate(sourceViewVerticalConstraints) 85 | // needsLayout = true 86 | } 87 | } 88 | 89 | func showRichTextView() { 90 | NSAnimationContext.runAnimationGroup { context in 91 | context.duration = 0 92 | context.allowsImplicitAnimation = false 93 | 94 | sourceView.isHidden = true 95 | richTextView.isHidden = false 96 | NSLayoutConstraint.deactivate(sourceViewVerticalConstraints) 97 | NSLayoutConstraint.activate(richTextViewVerticalConstraints) 98 | } 99 | } 100 | 101 | private var cancellables = Set() 102 | 103 | func update(with viewModel: CellViewModel, tableView: NotebookTableView) { 104 | // Store previous cell state 105 | // Note that this must happen before self.viewModel is updated 106 | self.viewModel?.selectedRange = sourceView.textView.selectedRange() 107 | 108 | // Note that this must happen before all subscribers 109 | self.viewModel = viewModel 110 | 111 | self.tableView = tableView 112 | 113 | // MARK: - Update views based on viewModel 114 | sourceView.uniqueUndoManager = viewModel.undoManager 115 | sourceView.textView.string = viewModel.source 116 | sourceView.textView.setSelectedRange(viewModel.selectedRange) 117 | 118 | clearOutputSubviews() 119 | if let outputs = viewModel.outputs { 120 | for output in outputs { 121 | appendOutputSubview(output) 122 | } 123 | } 124 | 125 | // MARK: - Subscribers 126 | 127 | cancellables.removeAll() 128 | 129 | viewModel.appendedOutput 130 | .sink { [weak self] output in 131 | self?.appendOutputSubview(output) 132 | } 133 | .store(in: &cancellables) 134 | 135 | viewModel.$cellType 136 | .removeDuplicates() 137 | .sink { [weak self] cellType in 138 | self?.showSourceView() 139 | self?.sourceView.textView.setWraps(cellType != .code) 140 | } 141 | .store(in: &cancellables) 142 | 143 | viewModel.$isEditingMarkdown 144 | .sink { [weak self] isEditingMarkdown in 145 | if self?.viewModel.cellType == .markdown && isEditingMarkdown { 146 | self?.showSourceView() 147 | } 148 | } 149 | .store(in: &cancellables) 150 | 151 | viewModel.$renderedMarkdown 152 | .sink { [weak self] renderedMarkdown in 153 | if self?.viewModel.cellType == .markdown { 154 | self?.richTextView.textStorage?.setAttributedString(renderedMarkdown) 155 | self?.richTextView.invalidateIntrinsicContentSize() 156 | self?.showRichTextView() 157 | } 158 | } 159 | .store(in: &cancellables) 160 | 161 | viewModel.$isExecuting 162 | .sink { [weak self] isExecuting in 163 | self?.alphaValue = isExecuting ? 0.5 : 1.0 164 | } 165 | .store(in: &cancellables) 166 | } 167 | 168 | func clearOutputSubviews() { 169 | for view in outputStackView.arrangedSubviews { 170 | view.removeFromSuperview() 171 | if let imageView = view as? NSImageView { 172 | reusableImageViews.append(imageView) 173 | } else if let textView = view as? OutputTextView { 174 | reusableTextViews.append(textView) 175 | } 176 | } 177 | } 178 | 179 | func appendOutputSubview(_ output: Output) { 180 | switch output { 181 | case .stream(let output): appendOutputTextSubview(output.text) 182 | case .displayData(let output): 183 | if let image = output.data.image { appendOutputImageSubview(image.value) } 184 | else if let htmlText = output.data.markdownText { appendOutputTextSubview(htmlText.value) } 185 | else if let markdownText = output.data.markdownText { appendOutputTextSubview(markdownText.value) } 186 | else if let plainText = output.data.plainText { appendOutputTextSubview(plainText.value) } 187 | case .executeResult(let output): 188 | if let image = output.data.image { appendOutputImageSubview(image.value) } 189 | else if let htmlText = output.data.markdownText { appendOutputTextSubview(htmlText.value) } 190 | else if let markdownText = output.data.markdownText { appendOutputTextSubview(markdownText.value) } 191 | else if let plainText = output.data.plainText { appendOutputTextSubview(plainText.value) } 192 | case .error(let output): appendOutputTextSubview(output.traceback.joined(separator: "\n")) 193 | } 194 | } 195 | 196 | func appendOutputTextSubview(_ text: String) { 197 | let textView: OutputTextView 198 | if let reusedTextView = reusableTextViews.popLast() { 199 | textView = reusedTextView 200 | } else { 201 | textView = OutputTextView() 202 | textView.customDelegate = self 203 | textView.translatesAutoresizingMaskIntoConstraints = false 204 | textView.setContentHuggingPriority(.required, for: .vertical) 205 | } 206 | 207 | textView.string = text.trimmingCharacters(in: Foundation.CharacterSet.whitespacesAndNewlines).replacing(/\[\d+[\d;]*m/, with: "") 208 | outputStackView.addArrangedSubview(textView) 209 | } 210 | 211 | func appendOutputImageSubview(_ image: NSImage) { 212 | let imageView: NSImageView 213 | if let reusedImageView = reusableImageViews.popLast() { 214 | imageView = reusedImageView 215 | } else { 216 | imageView = NSImageView() 217 | imageView.imageAlignment = .alignTopLeft 218 | imageView.translatesAutoresizingMaskIntoConstraints = false 219 | imageView.setContentHuggingPriority(.required, for: .vertical) 220 | } 221 | imageView.image = image 222 | outputStackView.addArrangedSubview(imageView) 223 | imageView.leadingAnchor.constraint(equalTo: outputStackView.leadingAnchor).isActive = true 224 | } 225 | 226 | func runCell() { 227 | if viewModel.cellType == .markdown && viewModel.isEditingMarkdown { 228 | viewModel.renderMarkdown() 229 | viewModel.isEditingMarkdown = false 230 | } else if viewModel.cellType == .code { 231 | viewModel.notebookViewModel.notebook.dirty = true 232 | viewModel.isExecuting = true 233 | 234 | clearOutputSubviews() 235 | viewModel.clearOutputs() 236 | JupyterService.shared.webSocketSend(code: viewModel.cell.source.value) { [weak viewModel] msg in 237 | switch msg.channel { 238 | case .iopub: 239 | var output: Output? 240 | switch msg.content { 241 | case .stream(let content): 242 | output = .stream(content) 243 | case .executeResult(let content): 244 | output = .executeResult(content) 245 | case .displayData(let content): 246 | output = .displayData(content) 247 | case .error(let content): 248 | output = .error(content) 249 | default: break 250 | } 251 | if let output { 252 | Task.detached { @MainActor in 253 | viewModel!.appendOutput(output) 254 | } 255 | } 256 | case .shell: 257 | switch msg.content { 258 | case .executeReply(_): 259 | Task.detached { @MainActor in 260 | viewModel!.isExecuting = false 261 | } 262 | default: break 263 | } 264 | } 265 | } 266 | } 267 | } 268 | 269 | private func selectCurrentRow() { 270 | tableView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false) 271 | } 272 | } 273 | 274 | extension CellView: SourceViewDelegate { 275 | func didChangeText(_ sourceView: SourceView) { 276 | viewModel.source = sourceView.textView.string 277 | } 278 | 279 | func didCommit(_ sourceView: SourceView) { 280 | tableView.runCellSelectBelow() 281 | endEditMode(sourceView) 282 | } 283 | 284 | func previousCell(_ sourceView: SourceView) { 285 | if row == 0 { return } 286 | tableView.selectCellAbove() 287 | let textView = tableView.selectedCellView!.sourceView.textView 288 | textView.setSelectedRange(NSRange(location: textView.string.count, length: 0)) 289 | window?.makeFirstResponder(textView) 290 | } 291 | 292 | func nextCell(_ sourceView: SourceView) { 293 | if row == tableView.numberOfRows - 1 { return } 294 | tableView.selectCellBelow() 295 | let textView = tableView.selectedCellView!.sourceView.textView 296 | textView.setSelectedRange(NSRange(location: 0, length: 0)) 297 | window?.makeFirstResponder(textView) 298 | } 299 | 300 | func didBecomeFirstResponder(_ sourceView: SourceView) { 301 | viewModel.isEditingMarkdown = true 302 | selectCurrentRow() 303 | } 304 | 305 | func endEditMode(_ sourceView: SourceView) { 306 | window?.makeFirstResponder(tableView) 307 | } 308 | 309 | func save() { 310 | tableView.save() 311 | } 312 | } 313 | 314 | extension CellView: OutputTextViewDelegate { 315 | func didBecomeFirstResponder(_ textView: OutputTextView) { 316 | selectCurrentRow() 317 | } 318 | } 319 | 320 | extension CellView: RichTextViewDelegate { 321 | func didBecomeFirstResponder(_ textView: RichTextView) { 322 | selectCurrentRow() 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /Jim/Cell/CellViewModel.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import Down 4 | 5 | class CellViewModel: ObservableObject { 6 | let cell: Cell 7 | let notebookViewModel: NotebookViewModel 8 | 9 | let undoManager = UndoManager() 10 | var selectedRange = NSRange(location: 0, length: 0) 11 | 12 | @Published var isExecuting = false 13 | @Published var isEditingMarkdown = false 14 | @Published var renderedMarkdown = NSAttributedString() 15 | 16 | var outputs: [Output]? { 17 | cell.outputs 18 | } 19 | 20 | private let appendedOutputSubject = PassthroughSubject() 21 | var appendedOutput: AnyPublisher { 22 | appendedOutputSubject.eraseToAnyPublisher() 23 | } 24 | 25 | var dirty = false 26 | 27 | var source: String { 28 | get { cell.source.value } 29 | set { 30 | dirty = dirty || (newValue != cell.source.value) 31 | cell.source.value = newValue 32 | } 33 | } 34 | 35 | @Published var cellType: CellType { 36 | didSet { 37 | if cellType != oldValue && cellType == .markdown { 38 | isEditingMarkdown = true 39 | } 40 | cell.cellType = cellType 41 | } 42 | } 43 | 44 | init(cell: Cell, notebookViewModel: NotebookViewModel) { 45 | self.cell = cell 46 | self.notebookViewModel = notebookViewModel 47 | self.cellType = cell.cellType 48 | if cellType == .markdown { 49 | renderMarkdown() 50 | } 51 | } 52 | 53 | func renderMarkdown() { 54 | let down = Down(markdownString: source) 55 | if let attributedString = try? down.toAttributedString(styler: MarkdownStyler.shared) { 56 | renderedMarkdown = attributedString 57 | } else { 58 | print("Error parsing markdown: \(source)") 59 | } 60 | } 61 | 62 | func appendOutput(_ output: Output) { 63 | cell.outputs?.append(output) 64 | appendedOutputSubject.send(output) 65 | } 66 | 67 | func clearOutputs() { 68 | cell.outputs?.removeAll() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Jim/Cell/MarkdownStyler.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Down 3 | 4 | class MarkdownStyler: DownStyler { 5 | static let shared = MarkdownStyler() 6 | 7 | init() { 8 | var listItemOptions = ListItemOptions() 9 | var quoteStripeOptions = QuoteStripeOptions() 10 | let thematicBreakOptions = ThematicBreakOptions() 11 | let codeBlockOptions = CodeBlockOptions() 12 | 13 | listItemOptions.spacingAbove = 0 14 | listItemOptions.spacingBelow = 0 15 | 16 | quoteStripeOptions.thickness = 5 17 | quoteStripeOptions.spacingAfter = 16 18 | 19 | let bodyStyle = NSMutableParagraphStyle() 20 | bodyStyle.paragraphSpacingBefore = 14 21 | bodyStyle.paragraphSpacing = 14 22 | 23 | let heading1Style = NSMutableParagraphStyle() 24 | heading1Style.paragraphSpacingBefore = 9 25 | heading1Style.paragraphSpacing = 9 26 | 27 | let secondaryHeadingStyle = NSMutableParagraphStyle() 28 | secondaryHeadingStyle.paragraphSpacingBefore = 14 29 | secondaryHeadingStyle.paragraphSpacing = 14 30 | 31 | let codeStyle = NSMutableParagraphStyle() 32 | 33 | var paragraphStyles = StaticParagraphStyleCollection() 34 | paragraphStyles.body = bodyStyle 35 | paragraphStyles.code = codeStyle 36 | paragraphStyles.heading1 = heading1Style 37 | paragraphStyles.heading2 = secondaryHeadingStyle 38 | paragraphStyles.heading3 = secondaryHeadingStyle 39 | paragraphStyles.heading4 = secondaryHeadingStyle 40 | paragraphStyles.heading5 = secondaryHeadingStyle 41 | paragraphStyles.heading6 = secondaryHeadingStyle 42 | 43 | let downStylerConfiguration = DownStylerConfiguration( 44 | fonts: MarkdownStyler.fontCollection(), 45 | colors: MarkdownStyler.colorCollection, 46 | paragraphStyles: paragraphStyles, 47 | listItemOptions: listItemOptions, 48 | quoteStripeOptions: quoteStripeOptions, 49 | thematicBreakOptions: thematicBreakOptions, 50 | codeBlockOptions: codeBlockOptions 51 | ) 52 | 53 | super.init(configuration: downStylerConfiguration) 54 | } 55 | 56 | static func fontCollection() -> FontCollection { 57 | var fonts = StaticFontCollection() 58 | fonts.body = fonts.body.withSize(14) 59 | fonts.heading1 = fonts.heading1.withSize(26) 60 | fonts.heading2 = fonts.heading2.withSize(22) 61 | fonts.heading3 = fonts.heading3.withSize(18) 62 | fonts.heading4 = fonts.heading4.withSize(14) 63 | fonts.heading5 = NSFontManager.shared.convert(fonts.heading5.withSize(14), toHaveTrait: .italicFontMask) 64 | fonts.heading6 = NSFontManager.shared.convert(fonts.heading6.withSize(14), toHaveTrait: .italicFontMask) 65 | fonts.code = fonts.code.withSize(14) 66 | fonts.listItemPrefix = fonts.body 67 | return fonts 68 | } 69 | 70 | private static func font(for descriptor: NSFontDescriptor?, fallback fallbackFont: NSFont) -> NSFont { 71 | if let descriptor = descriptor { 72 | return NSFont(descriptor: descriptor, size: 0)! 73 | } else { 74 | return fallbackFont 75 | } 76 | } 77 | 78 | static var colorCollection: ColorCollection { 79 | var colors = StaticColorCollection() 80 | colors.body = .labelColor 81 | colors.heading1 = .labelColor 82 | colors.heading2 = .labelColor 83 | colors.heading3 = .labelColor 84 | colors.code = .labelColor 85 | colors.link = .linkColor 86 | colors.listItemPrefix = .labelColor 87 | colors.quote = .labelColor 88 | colors.quoteStripe = .init(red: 0.933, green: 0.933, blue: 0.933, alpha: 1) 89 | colors.codeBlockBackground = .clear 90 | colors.thematicBreak = .init(red: 0.933, green: 0.933, blue: 0.933, alpha: 1) 91 | return colors 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Jim/Cell/MinimalTextView.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class MinimalTextView: NSTextView { 4 | var verticalPadding: CGFloat = 5 5 | 6 | private var frameObservation: NSKeyValueObservation? 7 | 8 | private(set) var wraps = true 9 | 10 | func setWraps(_ wraps: Bool, invalidate: Bool = true) { 11 | textContainer?.widthTracksTextView = wraps 12 | // Needed else horizontal scrollbar shows when the content doesn't overflow horizontally 13 | isHorizontallyResizable = !wraps 14 | // Only perform layout changes if the value actually changed 15 | if invalidate && wraps != self.wraps { 16 | // If we don't reset the container width it maintains the old width and wraps, despite 17 | // isHorizontallyResizable changing. 18 | if !wraps { 19 | textContainer?.size.width = .greatestFiniteMagnitude 20 | } 21 | invalidateIntrinsicContentSize() 22 | } 23 | self.wraps = wraps 24 | } 25 | 26 | init(layoutManager: NSLayoutManager = NSLayoutManager()) { 27 | let textStorage = NSTextStorage() 28 | let textContainer = NSTextContainer() 29 | textStorage.addLayoutManager(layoutManager) 30 | layoutManager.addTextContainer(textContainer) 31 | super.init(frame: .zero, textContainer: textContainer) 32 | setup() 33 | } 34 | 35 | required init?(coder: NSCoder) { 36 | super.init(coder: coder) 37 | setup() 38 | } 39 | 40 | private var cachedIntrinsicContentSize = NSSize.zero 41 | 42 | private func setup() { 43 | _ = layoutManager // Force TextKit 1 44 | 45 | usesFontPanel = false 46 | isRichText = false 47 | smartInsertDeleteEnabled = false 48 | isAutomaticTextCompletionEnabled = false 49 | isAutomaticTextReplacementEnabled = false 50 | isAutomaticSpellingCorrectionEnabled = false 51 | isAutomaticQuoteSubstitutionEnabled = false 52 | allowsCharacterPickerTouchBarItem = false 53 | 54 | textContainerInset.height = verticalPadding 55 | 56 | // Needed else reused views may retain previous height 57 | isVerticallyResizable = true 58 | 59 | setWraps(wraps, invalidate: false) 60 | 61 | frameObservation = observe(\.frame) { [weak self] (_, _) in 62 | self?.invalidateIntrinsicContentSize() 63 | } 64 | } 65 | 66 | public override var intrinsicContentSize: NSSize { 67 | guard let textContainer = textContainer, 68 | let layoutManager = layoutManager else { 69 | fatalError("Expected textContainer and layoutManager to exist.") 70 | } 71 | layoutManager.ensureLayout(for: textContainer) 72 | let size = layoutManager.usedRect(for: textContainer).size 73 | return NSSize(width: wraps ? -1 : size.width + 2 * textContainerInset.width, 74 | height: size.height + 2 * textContainerInset.height) 75 | } 76 | 77 | override func didChangeText() { 78 | super.didChangeText() 79 | invalidateIntrinsicContentSize() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Jim/Cell/OutputStackView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class OutputStackView: NSStackView { 4 | 5 | override var isOpaque: Bool { true } 6 | 7 | override func addArrangedSubview(_ view: NSView) { 8 | super.addArrangedSubview(view) 9 | NSLayoutConstraint.activate([ 10 | view.widthAnchor.constraint(equalTo: widthAnchor), 11 | view.leadingAnchor.constraint(equalTo: leadingAnchor), 12 | ]) 13 | view.setContentHuggingPriority(.required, for: .vertical) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Jim/Cell/OutputTextView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | protocol OutputTextViewDelegate { 4 | func didBecomeFirstResponder(_ textView: OutputTextView) 5 | } 6 | 7 | class OutputTextView: MinimalTextView { 8 | override var isOpaque: Bool { true } 9 | 10 | var customDelegate: OutputTextViewDelegate? 11 | 12 | override init(layoutManager: NSLayoutManager = NSLayoutManager()) { 13 | super.init() 14 | setup() 15 | } 16 | 17 | required init?(coder: NSCoder) { 18 | super.init(coder: coder) 19 | setup() 20 | } 21 | 22 | private func setup() { 23 | font = Theme.shared.monoFont 24 | drawsBackground = true 25 | backgroundColor = .white 26 | isEditable = false 27 | } 28 | 29 | override func keyDown(with event: NSEvent) { 30 | nextResponder?.keyDown(with: event) 31 | } 32 | 33 | override func becomeFirstResponder() -> Bool { 34 | customDelegate?.didBecomeFirstResponder(self) 35 | return super.becomeFirstResponder() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Jim/Cell/RichTextView.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Down 3 | 4 | protocol RichTextViewDelegate { 5 | func didBecomeFirstResponder(_ textView: RichTextView) 6 | } 7 | 8 | class RichTextView: MinimalTextView { 9 | override var isOpaque: Bool { false } 10 | 11 | var customDelegate: RichTextViewDelegate? 12 | 13 | override init(layoutManager: NSLayoutManager = DownLayoutManager()) { 14 | super.init(layoutManager: layoutManager) 15 | setup() 16 | } 17 | 18 | required init?(coder: NSCoder) { 19 | super.init(coder: coder) 20 | setup() 21 | } 22 | 23 | private func setup() { 24 | isEditable = false 25 | isRichText = true 26 | font = Theme.shared.font 27 | } 28 | 29 | override func keyDown(with event: NSEvent) { 30 | nextResponder?.keyDown(with: event) 31 | } 32 | 33 | override func becomeFirstResponder() -> Bool { 34 | customDelegate?.didBecomeFirstResponder(self) 35 | return super.becomeFirstResponder() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Jim/Cell/RunButton.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class InnerRunButton: NSButton { 4 | override func resetCursorRects() { 5 | super.resetCursorRects() 6 | addCursorRect(bounds, cursor: .pointingHand) 7 | } 8 | 9 | override func becomeFirstResponder() -> Bool { 10 | print("Heyo") 11 | return super.becomeFirstResponder() 12 | } 13 | } 14 | 15 | class RunButton: NSView { 16 | let button = InnerRunButton() 17 | let progress = NSProgressIndicator() 18 | var callback: (() -> ())? 19 | var inProgress = false { 20 | didSet { 21 | progress.isHidden = !inProgress 22 | } 23 | } 24 | 25 | init() { 26 | super.init(frame: .zero) 27 | 28 | button.target = self 29 | button.action = #selector(onClick) 30 | button.bezelStyle = .recessed 31 | button.showsBorderOnlyWhileMouseInside = true 32 | button.isHidden = true 33 | 34 | progress.isIndeterminate = true 35 | progress.startAnimation(self) 36 | progress.style = .spinning 37 | progress.controlSize = .small 38 | progress.isHidden = true 39 | 40 | addSubview(button) 41 | addSubview(progress) 42 | 43 | translatesAutoresizingMaskIntoConstraints = false 44 | button.translatesAutoresizingMaskIntoConstraints = false 45 | progress.translatesAutoresizingMaskIntoConstraints = false 46 | 47 | NSLayoutConstraint.activate([ 48 | widthAnchor.constraint(equalTo: button.widthAnchor), 49 | heightAnchor.constraint(equalTo: button.heightAnchor), 50 | 51 | button.centerYAnchor.constraint(equalTo: centerYAnchor), 52 | button.centerXAnchor.constraint(equalTo: centerXAnchor), 53 | 54 | progress.centerYAnchor.constraint(equalTo: centerYAnchor), 55 | progress.centerXAnchor.constraint(equalTo: centerXAnchor), 56 | ]) 57 | 58 | button.setContentHuggingPriority(.required, for: .horizontal) 59 | button.setContentHuggingPriority(.required, for: .vertical) 60 | } 61 | 62 | required init?(coder: NSCoder) { 63 | fatalError("init(coder:) has not been implemented") 64 | } 65 | 66 | @objc private func onClick() { 67 | callback?() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Jim/Jim.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Jim/Jupyter/Cell.swift: -------------------------------------------------------------------------------- 1 | import AnyCodable 2 | import Foundation 3 | 4 | class Cell: Codable, Identifiable, Equatable { 5 | let id: String 6 | // TODO: don't think it should be var, should make a new cell... 7 | var cellType: CellType 8 | var source: StringOrArray 9 | var outputs: [Output]? 10 | let metadata: [String: AnyCodable]? 11 | let executionCount: Int? 12 | 13 | // Editor properties 14 | var isExecuting = false 15 | var selectedRange = NSRange(location: 0, length: 0) 16 | 17 | init(id: String? = nil, cellType: CellType = .code, source: StringOrArray = StringOrArray(""), outputs: [Output]? = [], metadata: [String: AnyCodable]? = nil) { 18 | self.id = id ?? UUID().uuidString 19 | self.cellType = cellType 20 | self.source = source 21 | self.outputs = outputs 22 | self.metadata = metadata 23 | self.executionCount = nil // TODO 24 | } 25 | 26 | static func == (lhs: Cell, rhs: Cell) -> Bool { 27 | lhs.id == rhs.id 28 | } 29 | 30 | convenience init(from cell: Cell) { 31 | self.init(cellType: cell.cellType, source: cell.source, outputs: cell.outputs) 32 | } 33 | 34 | required init(from decoder: Decoder) throws { 35 | let container = try decoder.container(keyedBy: CodingKeys.self) 36 | self.id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString 37 | self.cellType = try container.decode(CellType.self, forKey: .cellType) 38 | self.source = try container.decode(StringOrArray.self, forKey: .source) 39 | self.outputs = try container.decodeIfPresent([Output].self, forKey: .outputs) 40 | self.metadata = try container.decodeIfPresent([String: AnyCodable].self, forKey: .metadata) 41 | self.executionCount = try container.decodeIfPresent(Int.self, forKey: .executionCount) 42 | } 43 | 44 | enum CodingKeys: CodingKey { 45 | case id 46 | case cellType 47 | case source 48 | case outputs 49 | case metadata 50 | case executionCount 51 | } 52 | 53 | func encode(to encoder: Encoder) throws { 54 | var container = encoder.container(keyedBy: CodingKeys.self) 55 | if encoder.userInfo[JupyterService.nbformatUserInfoKey] as! (Int, Int) >= (4, 5) { 56 | try container.encode(self.id, forKey: .id) 57 | } 58 | try container.encode(self.cellType, forKey: .cellType) 59 | try container.encode(self.source, forKey: .source) 60 | try container.encodeIfPresent(self.outputs, forKey: .outputs) 61 | if let metadata { 62 | try container.encode(metadata, forKey: .metadata) 63 | } else { 64 | try container.encode([String: AnyCodable](), forKey: .metadata) 65 | } 66 | // TODO: Make Cell an enum? 67 | switch self.cellType { 68 | case .code: try container.encode(self.executionCount, forKey: .executionCount) 69 | default: break 70 | } 71 | } 72 | } 73 | 74 | enum CellType: String, Codable { 75 | case raw, markdown, code 76 | } 77 | -------------------------------------------------------------------------------- /Jim/Jupyter/Content.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Content: Codable, Hashable, Comparable { 4 | let name: String 5 | let path: String 6 | let lastModified: Date 7 | let created: Date 8 | let content: [Content]? 9 | let size: Int? 10 | let type: ContentType 11 | 12 | func hash(into hasher: inout Hasher) { 13 | hasher.combine(path) 14 | } 15 | 16 | static func <(lhs: Self, rhs: Self) -> Bool { 17 | (lhs.type, lhs.name.lowercased()) < (rhs.type, rhs.name.lowercased()) 18 | } 19 | } 20 | 21 | enum ContentType: String, Codable, Comparable { 22 | case directory, notebook, file 23 | 24 | private static func minimum(_ lhs: Self, _ rhs: Self) -> Self { 25 | switch (lhs, rhs) { 26 | case (.directory, _), (_, .directory): 27 | return .directory 28 | case (.notebook, _), (_, .notebook): 29 | return .notebook 30 | case (.file, _), (_, .file): 31 | return .file 32 | } 33 | } 34 | 35 | static func <(lhs: Self, rhs: Self) -> Bool { 36 | (lhs != rhs) && (lhs == Self.minimum(lhs, rhs)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Jim/Jupyter/JupyterService.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import AnyCodable 3 | import SwiftUI 4 | 5 | enum JupyterError: Error { 6 | case notAuthenticated, unknownError, decodeError, encodeError 7 | case serverError(JupyterServerError) 8 | } 9 | 10 | struct JupyterServerError: Codable { 11 | let message: String 12 | let reason: String? 13 | } 14 | 15 | class JupyterService { 16 | static let shared = JupyterService() 17 | 18 | var baseUrl: String? 19 | private var token: String? 20 | private var xsrf: String? 21 | 22 | private let encoder: JSONEncoder 23 | private let decoder: JSONDecoder 24 | 25 | private var activeSession: Session? 26 | private var sessions = [Session: URLSessionWebSocketTask?]() 27 | private var executeRequests = [String: (Cancellable, PassthroughSubject)]() 28 | 29 | static var nbformatUserInfoKey: CodingUserInfoKey { 30 | .init(rawValue: "nbformat")! 31 | } 32 | 33 | init() { 34 | let dateFormatter = DateFormatter() 35 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'" 36 | encoder = JSONEncoder() 37 | encoder.keyEncodingStrategy = .convertToSnakeCase 38 | encoder.dateEncodingStrategy = .formatted(dateFormatter) 39 | decoder = JSONDecoder() 40 | decoder.keyDecodingStrategy = .convertFromSnakeCase 41 | decoder.dateDecodingStrategy = .formatted(dateFormatter) 42 | } 43 | 44 | func login(baseUrl: String, token: String) async -> Bool { 45 | let url = URL(string: baseUrl)! 46 | var request = URLRequest(url: url) 47 | request.setValue("token \(token)", forHTTPHeaderField: "Authorization") 48 | if let _ = try? await URLSession.shared.data(for: request), 49 | let xsrf = HTTPCookieStorage.shared.cookies?.first(where: { $0.name == "_xsrf" })?.value { 50 | self.baseUrl = baseUrl 51 | self.xsrf = xsrf 52 | return true 53 | } 54 | return false 55 | } 56 | 57 | private func makeRequest(path: String, method: String, json: [String: Any]? = nil, jsonData: Data? = nil) -> URLRequest? { 58 | guard let baseUrl, let xsrf else { return nil } 59 | assert(!(json != nil && jsonData != nil), "Cannot accept both json and jsonData") 60 | 61 | let timestamp = Int(1000 * Date().timeIntervalSince1970) 62 | let urlText = baseUrl + path + "?" + String(timestamp) 63 | let url = URL(string: urlText)! 64 | var request = URLRequest(url: url) 65 | request.httpMethod = method 66 | request.setValue(xsrf, forHTTPHeaderField: "X-XSRFToken") 67 | 68 | if let jsonData { 69 | request.httpBody = jsonData 70 | } else if let json { 71 | request.httpBody = try! JSONSerialization.data(withJSONObject: json) 72 | } 73 | if request.httpBody != nil { 74 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 75 | } 76 | 77 | return request 78 | } 79 | 80 | func request(path: String, method: String = "GET", json: [String: Any]? = nil, jsonData: Data? = nil) async -> Result { 81 | guard let request = makeRequest(path: path, method: method, json: json, jsonData: jsonData) else { 82 | return .failure(JupyterError.notAuthenticated) 83 | } 84 | 85 | let data: Data 86 | let response: URLResponse 87 | do { 88 | (data, response) = try await URLSession.shared.data(for: request) 89 | } catch { 90 | print("Error on request \(request):", error) 91 | return .failure(JupyterError.unknownError) 92 | } 93 | 94 | if let httpResponse = response as? HTTPURLResponse { 95 | if httpResponse.statusCode == 500 { 96 | let result = try! decoder.decode(JupyterServerError.self, from: data) 97 | return .failure(JupyterError.serverError(result)) 98 | } 99 | } 100 | 101 | return .success(data) 102 | } 103 | 104 | func decode(type: T.Type, data: Data) -> Result { 105 | do { 106 | let result = try decoder.decode(T.self, from: data) 107 | return .success(result) 108 | } catch { 109 | print("Error decoding response of type \(T.self):", error) 110 | print("Data:", String(data: data, encoding: .utf8)!.prefix(1000)) 111 | return .failure(JupyterError.decodeError) 112 | } 113 | } 114 | 115 | func getContent(_ path: String = "", type: T.Type) async -> Result { 116 | let data = await request(path: "api/contents/" + path.addingPercentEncoding(withAllowedCharacters: .alphanumerics)!) 117 | return data.flatMap { decode(type: type, data: $0) } 118 | } 119 | 120 | func updateContent(_ path: String, content: T) async -> Result { 121 | let contentJson: Data 122 | do { 123 | if let notebook = content as? Notebook { 124 | encoder.userInfo[JupyterService.nbformatUserInfoKey] = (notebook.content.nbformat, notebook.content.nbformatMinor) 125 | } 126 | contentJson = try encoder.encode(content) 127 | } catch { 128 | print("Error encoding content of type \(T.self)") 129 | return .failure(JupyterError.encodeError) 130 | } 131 | let data = await request(path: "api/contents/" + path.addingPercentEncoding(withAllowedCharacters: .alphanumerics)!, method: "PUT", jsonData: contentJson) 132 | return data.flatMap { decode(type: Content.self, data: $0) } 133 | } 134 | 135 | func createSession(name: String, path: String) async -> Result { 136 | // TODO: make kernel an arg 137 | let data = await request(path: "api/sessions", method: "POST", json: ["kernel": ["name": "python3"], "name": name, "path": path, "type": ContentType.notebook.rawValue]) 138 | let result = data.flatMap { decode(type: Session.self, data: $0) } 139 | if case let .success(session) = result { 140 | self.activeSession = session 141 | if self.sessions[session] == nil { 142 | self.sessions[session] = nil 143 | } 144 | } 145 | return result 146 | } 147 | 148 | func interruptKernel() async -> JupyterError? { 149 | switch await request(path: "api/kernels/\(activeSession!.kernel.id)/interrupt", method: "POST") { 150 | case .success(_): return nil 151 | case .failure(let error): 152 | print("Failed to interrupt:", error) 153 | return error 154 | } 155 | } 156 | 157 | func restartKernel() async -> Result { 158 | let data = await request(path: "api/kernels/\(activeSession!.kernel.id)/restart", method: "POST") 159 | return data.flatMap { decode(type: Kernel.self, data: $0) } 160 | } 161 | 162 | func webSocketTask(_ session: Session) { 163 | guard let baseUrl else { return } 164 | if self.sessions[session] != nil { return } 165 | // TODO: why don't I need xsrf here? 166 | let url = URL(string: baseUrl + "api/kernels/\(session.kernel.id)/channels?session_id=\(session.id)")! 167 | var request = URLRequest(url: url) 168 | request.timeoutInterval = 5 169 | let task = URLSession.shared.webSocketTask(with: request) 170 | self.sessions[session] = task 171 | task.resume() 172 | recieveAll(task) 173 | } 174 | 175 | private func recieveAll(_ task: URLSessionWebSocketTask) { 176 | task.receive { result in 177 | switch result { 178 | case .failure(let error): 179 | print("Failed to receive message, exiting recieve handler: \(error)") 180 | return 181 | case .success(let message): 182 | switch message { 183 | case .string(let text): 184 | do { 185 | let msg = try self.decoder.decode(Message.self, from: text.data(using: .utf8)!) 186 | if let msgId = msg.parentHeader?.msgId, 187 | let subject = self.executeRequests[msgId]?.1 { 188 | subject.send(msg) 189 | } 190 | } catch { 191 | print("Error decoding data. Error: \(error), Data: \(text)") 192 | } 193 | case .data(let data): 194 | print("Received binary message: \(data)") 195 | @unknown default: 196 | fatalError() 197 | } 198 | } 199 | self.recieveAll(task) 200 | } 201 | } 202 | 203 | func webSocketSend(code: String, handler: @escaping (Message) -> ()) { 204 | // TODO: Where does username come from? 205 | let msgId = UUID().uuidString // TODO: move into Message? 206 | let msg = Message(header: MessageHeader(msgId: msgId, session: self.activeSession!.id, username: "seem", version: "5.3", date: Date(), msgType: .executeRequest), parentHeader: nil, content: MessageContent.executeRequest(.init(code: code, silent: false)), channel: .shell) 207 | let msgD: Data 208 | do { 209 | msgD = try self.encoder.encode(msg) 210 | } catch { 211 | print("Error decoding message to send: \(error)") 212 | return 213 | } 214 | let msgS = String(data: msgD, encoding: .utf8)! 215 | let codeMsgD = URLSessionWebSocketTask.Message.string(msgS) 216 | let subject = PassthroughSubject() 217 | let cancellable = subject.sink { msg in 218 | handler(msg) 219 | } 220 | self.executeRequests[msgId] = (cancellable, subject) 221 | self.sessions[self.activeSession!]!!.send(codeMsgD) { error in 222 | if let error = error { 223 | print("Error: \(error)") 224 | return 225 | } 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /Jim/Jupyter/Message.swift: -------------------------------------------------------------------------------- 1 | import AnyCodable 2 | import Foundation 3 | 4 | struct Message: Codable { 5 | // let buffers: [] // TODO: need? 6 | let header: MessageHeader 7 | let parentHeader: MessageHeader? 8 | let content: MessageContent 9 | let channel: MessageChannel 10 | let metadata: [String: AnyCodable] 11 | 12 | init(header: MessageHeader, parentHeader: MessageHeader?, content: MessageContent, channel: MessageChannel, metadata: [String: AnyCodable] = [:]) { 13 | self.header = header 14 | self.parentHeader = parentHeader 15 | self.content = content 16 | self.channel = channel 17 | self.metadata = metadata 18 | } 19 | 20 | enum CodingKeys: String, CodingKey { 21 | case header, parentHeader, content, channel, metadata 22 | } 23 | 24 | init(from decoder: Decoder) throws { 25 | let container = try decoder.container(keyedBy: CodingKeys.self) 26 | if let parentHeader = try? container.decode(MessageHeader.self, forKey: .parentHeader) { 27 | self.parentHeader = parentHeader 28 | } else { 29 | let parentHeader = try container.decode([String: String].self, forKey: .parentHeader) 30 | if parentHeader == [:] { 31 | self.parentHeader = nil 32 | } else { 33 | throw DecodingError.dataCorruptedError(forKey: .parentHeader, in: container, debugDescription: "MessageHeader value cannot be decoded") 34 | } 35 | } 36 | self.channel = try container.decode(MessageChannel.self, forKey: .channel) 37 | self.metadata = try container.decode([String: AnyCodable].self, forKey: .metadata) 38 | self.header = try container.decode(MessageHeader.self, forKey: .header) 39 | switch self.header.msgType { 40 | case .executeRequest: self.content = .executeRequest(try container.decode(ExecuteRequestContent.self, forKey: .content)) 41 | case .executeReply: self.content = .executeReply(try container.decode(ExecuteReplyContent.self, forKey: .content)) 42 | case .executeResult: self.content = .executeResult(try container.decode(ExecuteResultOutput.self, forKey: .content)) 43 | case .status: self.content = .status(try container.decode(StatusContent.self, forKey: .content)) 44 | case .kernelInfoRequest: self.content = .kernelInfoRequest(try container.decode(KernelInfoRequestContent.self, forKey: .content)) 45 | case .executeInput: self.content = .executeInput(try container.decode(ExecuteInputContent.self, forKey: .content)) 46 | case .stream: self.content = .stream(try container.decode(StreamOutput.self, forKey: .content)) 47 | case .error: self.content = .error(try container.decode(ErrorOutput.self, forKey: .content)) 48 | case .displayData: self.content = .displayData(try container.decode(DisplayDataOutput.self, forKey: .content)) 49 | case .commOpen: self.content = .commOpen(try container.decode(CommOpenContent.self, forKey: .content)) 50 | case .commMessage: self.content = .commMessage(try container.decode(CommMessageContent.self, forKey: .content)) 51 | case .shutdownRequest: self.content = .shutdownRequest(try container.decode(ShutdownRequestContent.self, forKey: .content)) 52 | case .shutdownReply: self.content = .shutdownReply(try container.decode(ShutdownReplyContent.self, forKey: .content)) 53 | } 54 | } 55 | 56 | func encode(to encoder: Encoder) throws { 57 | var container = encoder.container(keyedBy: CodingKeys.self) 58 | if self.parentHeader == nil { 59 | try container.encode(AnyCodable([:]), forKey: .parentHeader) 60 | } else { 61 | try container.encode(self.parentHeader, forKey: .parentHeader) 62 | } 63 | try container.encode(self.channel, forKey: .channel) 64 | try container.encode(self.metadata, forKey: .metadata) 65 | try container.encode(self.header, forKey: .header) 66 | switch self.content { 67 | case .executeRequest(let content): try container.encode(content, forKey: .content) 68 | case .executeReply(let content): try container.encode(content, forKey: .content) 69 | case .executeResult(let content): try container.encode(content, forKey: .content) 70 | case .status(let content): try container.encode(content, forKey: .content) 71 | case .kernelInfoRequest(let content): try container.encode(content, forKey: .content) 72 | case .executeInput(let content): try container.encode(content, forKey: .content) 73 | case .stream(let content): try container.encode(content, forKey: .content) 74 | case .error(let content): try container.encode(content, forKey: .content) 75 | case .displayData(let content): try container.encode(content, forKey: .content) 76 | case .commOpen(let content): try container.encode(content, forKey: .content) 77 | case .commMessage(let content): try container.encode(content, forKey: .content) 78 | case .shutdownRequest(let content): try container.encode(content, forKey: .content) 79 | case .shutdownReply(let content): try container.encode(content, forKey: .content) 80 | } 81 | } 82 | } 83 | 84 | enum MessageChannel: String, Codable { 85 | case shell, iopub 86 | } 87 | 88 | struct MessageHeader: Codable { 89 | let msgId: String 90 | let session: String 91 | let username: String 92 | let version: String 93 | let date: Date 94 | let msgType: MessageType 95 | 96 | init(msgId: String, session: String, username: String, version: String, date: Date, msgType: MessageType) { 97 | self.msgId = msgId 98 | self.session = session 99 | self.username = username 100 | self.version = version 101 | self.date = date 102 | self.msgType = msgType 103 | } 104 | } 105 | 106 | enum MessageType: String, Codable { 107 | case executeRequest = "execute_request" 108 | case executeReply = "execute_reply" 109 | case executeResult = "execute_result" 110 | case status 111 | case kernelInfoRequest = "kernel_info_request" 112 | case executeInput = "execute_input" 113 | case stream 114 | case error 115 | case displayData = "display_data" 116 | case commOpen = "comm_open" 117 | case commMessage = "comm_msg" 118 | case shutdownRequest = "shutdown_request" 119 | case shutdownReply = "shutdown_reply" 120 | // case execute_reply, inspect_request, inspect_reply, complete_request, complete_reply, history_request, history_reply, is_complete_request, is_complete_reply, connect_request, connect_reply, comm_info_request, comm_info_reply, kernel_info_request, kernel_info_reply, shutdown_request, shutdown_reply, interrupt_request, interrupt_reply, debug_request, debug_reply, stream, display_data, update_display_data, execute_input, execute_result, error, status, clear_output, debug_event, input_request, input_reply, comm_msg, comm_close 121 | } 122 | 123 | enum MessageContent { 124 | case executeRequest(ExecuteRequestContent) 125 | case executeReply(ExecuteReplyContent) 126 | case executeResult(ExecuteResultOutput) 127 | case displayData(DisplayDataOutput) 128 | case status(StatusContent) 129 | case kernelInfoRequest(KernelInfoRequestContent) 130 | case executeInput(ExecuteInputContent) 131 | case stream(StreamOutput) 132 | case error(ErrorOutput) 133 | case commOpen(CommOpenContent) 134 | case commMessage(CommMessageContent) 135 | case shutdownRequest(ShutdownRequestContent) 136 | case shutdownReply(ShutdownReplyContent) 137 | 138 | enum CodingKeys: String, CodingKey { 139 | case msgType 140 | } 141 | 142 | // init(from decoder: Decoder) throws { 143 | // let container = try decoder.container(keyedBy: CodingKeys.self) 144 | // let msgType = try container.decode(MessageType.self, forKey: .msgType) 145 | // let typeContainer = try decoder.singleValueContainer() 146 | // switch msgType { 147 | // case .executeRequest: self = .executeRequest(try typeContainer.decode(ExecuteRequestContent.self)) 148 | // case .status: self = .status(try typeContainer.decode(StatusContent.self)) 149 | // case .kernelInfoRequest: self = .kernelInfoRequest(try typeContainer.decode(KernelInfoRequestContent.self)) 150 | // } 151 | // } 152 | // 153 | // func encode(to encoder: Encoder) throws { 154 | // var container = encoder.container(keyedBy: CodingKeys.self) 155 | // var typeContainer = encoder.singleValueContainer() 156 | // switch self { 157 | // case .executeRequest(let content): 158 | // try typeContainer.encode(content) 159 | // try container.encode(MessageType.executeRequest, forKey: .msgType) 160 | // case .status(let content): 161 | // try typeContainer.encode(content) 162 | // try container.encode(MessageType.status, forKey: .msgType) 163 | // case .kernelInfoRequest(let content): 164 | // try typeContainer.encode(content) 165 | // try container.encode(MessageType.kernelInfoRequest, forKey: .msgType) 166 | // 167 | // } 168 | // } 169 | } 170 | 171 | struct CommOpenContent: Codable { 172 | let data: AnyCodable 173 | let commId: String 174 | let targetName: String 175 | let targetModule: String? 176 | } 177 | 178 | struct CommMessageContent: Codable { 179 | let data: AnyCodable 180 | let commId: String 181 | } 182 | 183 | struct ExecuteRequestContent: Codable { 184 | let code: String 185 | let silent: Bool// = false 186 | let storeHistory: Bool? 187 | let userExpressions: [String: AnyCodable]? 188 | let allowStdin: Bool? 189 | let stopOnError: Bool? 190 | 191 | init(code: String, silent: Bool, storeHistory: Bool? = nil, userExpressions: [String: AnyCodable]? = nil, allowStdin: Bool? = nil, stopOnError: Bool? = nil) { 192 | self.code = code 193 | self.silent = silent 194 | self.storeHistory = storeHistory 195 | self.userExpressions = userExpressions 196 | self.allowStdin = allowStdin 197 | self.stopOnError = stopOnError 198 | } 199 | } 200 | 201 | struct ExecuteReplyContent: Codable { 202 | let status: String // TODO: Make enum? 203 | let executionCount: Int 204 | let userExpressions: [String: AnyCodable]? 205 | let payload: [AnyCodable] // TODO: need? 206 | } 207 | 208 | struct StatusContent: Codable { 209 | let executionState: ExecutionState 210 | 211 | init(executionState: ExecutionState) { 212 | self.executionState = executionState 213 | } 214 | } 215 | 216 | enum ExecutionState: String, Codable { 217 | case busy, idle, starting 218 | } 219 | 220 | struct KernelInfoRequestContent: Codable { 221 | init() { } 222 | } 223 | 224 | struct ExecuteInputContent: Codable { 225 | let code: String 226 | let executionCount: Int 227 | } 228 | 229 | struct ShutdownRequestContent: Codable { 230 | let restart: Bool 231 | } 232 | 233 | struct ShutdownReplyContent: Codable { 234 | let status: String // TODO: make enum? 235 | let restart: Bool 236 | } 237 | -------------------------------------------------------------------------------- /Jim/Jupyter/Notebook.swift: -------------------------------------------------------------------------------- 1 | import AnyCodable 2 | import Foundation 3 | 4 | class Notebook: Codable { 5 | let name: String 6 | var id: String { name } 7 | let path: String 8 | var lastModified: Date 9 | let created: Date 10 | var content: NotebookContent 11 | var size: Int? 12 | let type: ContentType 13 | var dirty = false 14 | 15 | enum CodingKeys: CodingKey { 16 | case name 17 | case path 18 | case lastModified 19 | case created 20 | case content 21 | case size 22 | case type 23 | } 24 | } 25 | 26 | class NotebookContent: Codable { 27 | var cells: [Cell] 28 | let nbformat: Int 29 | let nbformatMinor: Int 30 | let metadata: [String: AnyCodable] 31 | } 32 | 33 | //struct NotebookMetadata: Codable { 34 | // let widgets: NotebookMetadataWidgets? 35 | //} 36 | // 37 | //struct NotebookMetadataWidgets: Codable { 38 | // let widgetState: NotebookMetadataWidgetState 39 | // 40 | // enum CodingKeys: String, CodingKey { 41 | // case widgetState = "application/vnd.jupyter.widget-state+json" 42 | // } 43 | //} 44 | // 45 | //struct NotebookMetadataWidgetState: Codable { 46 | // let state: [String: WidgetModel] 47 | // let versionMajor: Int 48 | // let versionMinor: Int 49 | //} 50 | // 51 | //struct WidgetModel: Codable { 52 | // let modelModule: String 53 | // let modelModuleVersion: String 54 | // let modelName: String 55 | // let state: AnyCodable 56 | //} 57 | -------------------------------------------------------------------------------- /Jim/Jupyter/Output.swift: -------------------------------------------------------------------------------- 1 | import AnyCodable 2 | import Foundation 3 | 4 | enum Output: Codable, Hashable, Identifiable { 5 | var id: Self { self } 6 | case stream(StreamOutput) 7 | case displayData(DisplayDataOutput) 8 | case executeResult(ExecuteResultOutput) 9 | case error(ErrorOutput) 10 | 11 | enum CodingKeys: String, CodingKey { 12 | case outputType 13 | } 14 | 15 | init(from decoder: Decoder) throws { 16 | let container = try decoder.container(keyedBy: CodingKeys.self) 17 | let outputType = try container.decode(OutputType.self, forKey: .outputType) 18 | let typeContainer = try decoder.singleValueContainer() 19 | switch outputType { 20 | case .stream: self = .stream(try typeContainer.decode(StreamOutput.self)) 21 | case .displayData: self = .displayData(try typeContainer.decode(DisplayDataOutput.self)) 22 | case .executeResult: self = .executeResult(try typeContainer.decode(ExecuteResultOutput.self)) 23 | case .error: self = .error(try typeContainer.decode(ErrorOutput.self)) 24 | } 25 | } 26 | 27 | func encode(to encoder: Encoder) throws { 28 | var typeContainer = encoder.singleValueContainer() 29 | switch self { 30 | case .stream(let output): try typeContainer.encode(output) 31 | case .displayData(let output): try typeContainer.encode(output) 32 | case .executeResult(let output): try typeContainer.encode(output) 33 | case .error(let output): try typeContainer.encode(output) 34 | } 35 | 36 | var container = encoder.container(keyedBy: CodingKeys.self) 37 | switch self { 38 | case .stream(_): try container.encode(OutputType.stream, forKey: .outputType) 39 | case .displayData(_): try container.encode(OutputType.displayData, forKey: .outputType) 40 | case .executeResult(_): try container.encode(OutputType.executeResult, forKey: .outputType) 41 | case .error(_): try container.encode(OutputType.error, forKey: .outputType) 42 | } 43 | } 44 | } 45 | 46 | enum OutputType: String, Codable { 47 | case stream 48 | case displayData = "display_data" 49 | case executeResult = "execute_result" 50 | case error 51 | } 52 | 53 | struct StreamOutput: Codable, Hashable { 54 | let name: StreamName 55 | let text: String 56 | } 57 | 58 | enum StreamName: String, Codable { 59 | case stderr, stdout 60 | } 61 | 62 | struct DisplayDataOutput: Codable, Hashable { 63 | let data: OutputData 64 | var metadata: [String: AnyCodable]? 65 | 66 | func encode(to encoder: Encoder) throws { 67 | var container = encoder.container(keyedBy: CodingKeys.self) 68 | try container.encode(self.data, forKey: .data) 69 | if let metadata { 70 | try container.encode(metadata, forKey: .metadata) 71 | } else { 72 | try container.encode([String: AnyCodable](), forKey: .metadata) 73 | } 74 | } 75 | } 76 | 77 | struct ExecuteResultOutput: Codable, Hashable { 78 | let data: OutputData 79 | var metadata: [String: AnyCodable]? 80 | let executionCount: Int? 81 | 82 | func encode(to encoder: Encoder) throws { 83 | var container = encoder.container(keyedBy: CodingKeys.self) 84 | try container.encode(self.data, forKey: .data) 85 | if let metadata { 86 | try container.encode(metadata, forKey: .metadata) 87 | } else { 88 | try container.encode([String: AnyCodable](), forKey: .metadata) 89 | } 90 | try container.encode(executionCount, forKey: .executionCount) 91 | } 92 | } 93 | 94 | struct OutputData: Codable, Hashable { 95 | let plainText: StringOrArray? 96 | let markdownText: StringOrArray? 97 | let htmlText: StringOrArray? 98 | let widgetView: WidgetView? 99 | let image: Base64Image? 100 | 101 | enum CodingKeys: String, CodingKey { 102 | case plainText = "text/plain" 103 | case markdownText = "text/markdown" 104 | case htmlText = "text/html" 105 | case image = "image/png" 106 | case widgetView = "application/vnd.jupyter.widget-view+json" 107 | } 108 | } 109 | 110 | struct WidgetView: Codable, Hashable { 111 | let modelId: String 112 | let versionMajor: Int 113 | let versionMinor: Int 114 | } 115 | 116 | struct ErrorOutput: Codable, Hashable { 117 | let traceback: [String] 118 | let ename: String 119 | let evalue: String 120 | } 121 | -------------------------------------------------------------------------------- /Jim/Jupyter/Session.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Session: Codable, Hashable { 4 | let id: String 5 | let name: String 6 | let path: String 7 | let type: ContentType 8 | let notebook: Self.Notebook 9 | let kernel: Kernel 10 | 11 | func hash(into hasher: inout Hasher) { 12 | hasher.combine(id) 13 | } 14 | 15 | static func == (lhs: Session, rhs: Session) -> Bool { 16 | lhs.id == rhs.id 17 | } 18 | 19 | struct Notebook: Codable { 20 | let name: String 21 | let path: String 22 | } 23 | } 24 | 25 | struct Kernel: Codable { 26 | let id: String 27 | let connections: Int 28 | let executionState: String 29 | let lastActivity: Date 30 | let name: String 31 | } 32 | -------------------------------------------------------------------------------- /Jim/Main/SidebarViewController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class SidebarViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource { 4 | @IBOutlet var tableView: NSTableView! 5 | 6 | let jupyter = JupyterService.shared 7 | var content: Content? { 8 | didSet { 9 | guard let content = content else { return } 10 | contents = content.content?.sorted() ?? [] 11 | tableView.reloadData() 12 | view.window?.title = content.name == "" ? "Files" : content.name 13 | } 14 | } 15 | var contents = [Content]() 16 | var path = [Content]() 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | Task { 21 | _ = await jupyter.login(baseUrl: "http://localhost:8999/", token: "testtoken123") 22 | getContent() 23 | } 24 | tableView.action = #selector(onTableClick) 25 | tableView.delegate = self 26 | tableView.dataSource = self 27 | } 28 | 29 | func getContent(_ path: String = "") { 30 | Task { 31 | switch await jupyter.getContent(path, type: Content.self) { 32 | case .success(let content): 33 | self.content = content 34 | case .failure(let error): 35 | print("Error getting content for path '\(path)':", error) 36 | } 37 | } 38 | } 39 | 40 | @objc private func onTableClick() { 41 | guard tableView.clickedRow != -1 else { return } 42 | let item = contents[tableView.clickedRow] 43 | if item.type == .directory { 44 | let prev = content 45 | getContent(item.path) 46 | if let prev { 47 | path.append(prev) 48 | } 49 | } else if item.type == .notebook { 50 | if let notebookViewController = (parent as? NSSplitViewController)?.children[1] as? NotebookViewController { 51 | notebookViewController.notebookSelected(path: item.path) 52 | } 53 | } 54 | } 55 | 56 | func numberOfRows(in tableView: NSTableView) -> Int { 57 | contents.count 58 | } 59 | 60 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 61 | guard let view = tableView.makeView(withIdentifier: tableColumn!.identifier, owner: self) as? NSTableCellView else { return nil } 62 | let item = contents[row] 63 | let systemSymbolName = item.type == .directory ? "folder.fill" : item.type == .notebook ? "text.book.closed" : "doc" 64 | view.imageView?.image = NSImage(systemSymbolName: systemSymbolName, accessibilityDescription: nil)! 65 | // Blue 66 | // view.imageView?.contentTintColor = item.type == .notebook ? NSColor.init(red: 0.071, green: 0.471, blue: 0.949, alpha: 1.0) : Theme.shared.sidebarTintColor 67 | // Orange 68 | view.imageView?.contentTintColor = item.type == .notebook ? NSColor.init(red: 0.921, green: 0.447, blue: 0.192, alpha: 1.0) : Theme.shared.sidebarTintColor 69 | // view.imageView?.contentTintColor = Theme.shared.sidebarTintColor 70 | view.textField?.stringValue = item.name 71 | return view 72 | } 73 | } 74 | 75 | // MARK: Back toolbar item 76 | 77 | extension SidebarViewController: NSToolbarItemValidation { 78 | func validateToolbarItem(_ item: NSToolbarItem) -> Bool { 79 | path.count > 0 80 | } 81 | 82 | @IBAction func backClicked(_ sender: NSView) { 83 | content = path.popLast() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Jim/Main/WindowController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class WindowController: NSWindowController { 4 | @IBOutlet var cellTypeComboBox: NSComboBox! 5 | } 6 | -------------------------------------------------------------------------------- /Jim/Notebook/NotebookTableRowView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | 4 | class NotebookTableRowView: NSTableRowView { 5 | // Never render the selected row's children as "emphasized" 6 | override var isEmphasized: Bool { get { false } set {} } 7 | 8 | override func drawSelection(in dirtyRect: NSRect) { 9 | // TODO: How do I get these values programmatically? 10 | let borderRect = NSInsetRect(self.bounds, 5, 7) 11 | 12 | let leftMarginRect = NSRect(x: borderRect.minX, y: borderRect.minY, width: 5, height: borderRect.height) 13 | NSColor(red: 0, green: 125/255, blue: 250/255, alpha: 1).setFill() 14 | NSBezierPath.init(roundedRect: leftMarginRect, xRadius: 2, yRadius: 2).fill() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Jim/Notebook/NotebookTableView.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class NotebookTableView: NSTableView { 4 | var viewModel: NotebookViewModel! 5 | 6 | override var isOpaque: Bool { true } 7 | 8 | var copyBuffer: Cell? 9 | var previouslyRemovedCell: Cell? { 10 | didSet { 11 | copyBuffer = previouslyRemovedCell 12 | } 13 | } 14 | var previouslyRemovedRow: Int? 15 | var selectedCellView: CellView? { 16 | view(atColumn: selectedColumn, row: selectedRow, makeIfNecessary: false) as? CellView 17 | } 18 | 19 | func selectCell(at tryRow: Int) { 20 | let row = tryRow < 0 ? 0 : tryRow >= numberOfRows ? numberOfRows - 1: tryRow 21 | selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false) 22 | scrollRowToVisible(row) 23 | } 24 | 25 | func selectCellAbove(_ n: Int = 1) { 26 | selectCell(at: selectedRow - n) 27 | } 28 | 29 | func selectCellBelow(_ n: Int = 1) { 30 | selectCell(at: selectedRow + n) 31 | } 32 | 33 | func selectFirstCell() { 34 | selectCell(at: 0) 35 | } 36 | 37 | func selectLastCell() { 38 | selectCell(at: numberOfRows - 1) 39 | } 40 | 41 | func enterEditMode() { 42 | guard let textView = selectedCellView?.sourceView.textView else { return } 43 | window?.makeFirstResponder(textView) 44 | textView.scrollRangeToVisible(textView.selectedRange()) 45 | } 46 | 47 | func insertCell(at row: Int, cell: Cell = Cell()) { 48 | viewModel.insertCell(cell, at: row) 49 | insertRows(at: .init(integer: row)) 50 | 51 | guard let previouslyRemovedRow else { return } 52 | if row < previouslyRemovedRow { 53 | self.previouslyRemovedRow = previouslyRemovedRow + 1 54 | } 55 | } 56 | 57 | func insertCellAbove(cell: Cell = Cell()) { 58 | insertCell(at: selectedRow, cell: cell) 59 | selectCellAbove() 60 | } 61 | 62 | func insertCellBelow(cell: Cell = Cell()) { 63 | insertCell(at: selectedRow + 1, cell: cell) 64 | selectCellBelow() 65 | } 66 | 67 | func removeCell(at row: Int) -> Cell { 68 | removeRows(at: .init(integer: row)) 69 | return viewModel.removeCell(at: row) 70 | } 71 | 72 | func moveCell(at row: Int, to: Int) { 73 | if to < 0 || to >= numberOfRows { return } 74 | let cell = removeCell(at: row) 75 | insertCell(at: to, cell: cell) 76 | selectCell(at: to) 77 | } 78 | 79 | func moveCellUp() { 80 | moveCell(at: selectedRow, to: selectedRow - 1) 81 | } 82 | 83 | func moveCellDown() { 84 | moveCell(at: selectedRow, to: selectedRow + 1) 85 | } 86 | 87 | func runCell() { 88 | selectedCellView?.runCell() 89 | } 90 | 91 | func runCellSelectBelow() { 92 | runCell() 93 | let row = selectedRow + 1 94 | if row == numberOfRows { 95 | insertCell(at: row) 96 | selectCellBelow() 97 | enterEditMode() 98 | } else { 99 | selectCellBelow() 100 | } 101 | } 102 | 103 | func cutCell() { 104 | let row = selectedRow 105 | previouslyRemovedRow = row 106 | previouslyRemovedCell = removeCell(at: row) 107 | if numberOfRows == 0 { 108 | insertCell(at: row) 109 | } 110 | selectCell(at: row) 111 | } 112 | 113 | func copyCell() { 114 | copyBuffer = viewModel.cell(at: selectedRow) 115 | } 116 | 117 | func pasteCellAbove() { 118 | guard let cell = copyBuffer else { return } 119 | insertCellAbove(cell: Cell(from: cell)) 120 | } 121 | 122 | func pasteCellBelow() { 123 | guard let cell = copyBuffer else { return } 124 | insertCellBelow(cell: Cell(from: cell)) 125 | } 126 | 127 | func undoCellDeletion() { 128 | guard let cell = previouslyRemovedCell, 129 | let row = previouslyRemovedRow else { return } 130 | insertCell(at: row, cell: cell) 131 | } 132 | 133 | func interruptKernel() { 134 | Task { await JupyterService.shared.interruptKernel() } 135 | } 136 | 137 | func restartKernel() { 138 | Task { await JupyterService.shared.restartKernel() } 139 | } 140 | 141 | func setCellType(_ cellType: CellType) { 142 | // TODO: make this undoable too? 143 | // TODO: more consistent way to access the cell? 144 | selectedCellView?.viewModel.cellType = cellType 145 | // TODO: very ugly 146 | let windowController = window!.windowController as! WindowController 147 | let title = cellType.rawValue.capitalized 148 | windowController.cellTypeComboBox.cell?.title = title 149 | } 150 | 151 | var keys = [UInt16]() 152 | var timer: Timer? 153 | 154 | override func reloadData() { 155 | super.reloadData() 156 | window?.makeFirstResponder(self) 157 | window?.title = viewModel.notebook.name 158 | } 159 | 160 | // TODO: really doesn't feel like the right place for jupyter/save logic 161 | func save() { 162 | Task { 163 | switch await viewModel.getLatestNotebook() { 164 | case .success(let diskNotebook): 165 | // TODO: need a margin? lab uses 500 166 | if diskNotebook.lastModified > viewModel.notebook.lastModified { 167 | let alert = makeAlert() 168 | let response = alert.runModal() 169 | switch response { 170 | case .alertFirstButtonReturn: 171 | await viewModel.updateNotebook() 172 | case .alertSecondButtonReturn: 173 | // TODO: do we need to handle cellViewModels if notebook is set? 174 | viewModel.notebook = diskNotebook 175 | viewModel.cellViewModels = [:] 176 | reloadData() 177 | default: break 178 | } 179 | } else { 180 | await viewModel.updateNotebook() 181 | } 182 | case .failure(let error): 183 | print("Failed to get content while saving notebook, error:", error) // TODO: show alert 184 | return 185 | } 186 | } 187 | } 188 | 189 | private func makeAlert() -> NSAlert { 190 | let alert = NSAlert() 191 | alert.messageText = "Failed to save \(viewModel.notebook.path)" 192 | alert.informativeText = "The content on disk is newer. Do you want to overwrite it with your changes or discard them and revert to the on disk content?" 193 | alert.alertStyle = .warning 194 | alert.addButton(withTitle: "Overwrite") 195 | alert.addButton(withTitle: "Discard") 196 | alert.addButton(withTitle: "Cancel") 197 | let discardButton = alert.buttons[1] 198 | discardButton.hasDestructiveAction = true 199 | let overwriteButton = alert.buttons[0] 200 | overwriteButton.hasDestructiveAction = true 201 | return alert 202 | } 203 | 204 | override func keyDown(with event: NSEvent) { 205 | let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) 206 | if event.keyCode == 36 && flags == .shift { // shift+enter 207 | runCellSelectBelow() 208 | } else if event.keyCode == 36 { // enter 209 | enterEditMode() 210 | } else if event.keyCode == 40 { // k 211 | selectCellAbove() 212 | } else if event.keyCode == 38 { // j 213 | selectCellBelow() 214 | } else if event.keyCode == 0 { // a 215 | insertCellAbove() 216 | } else if event.keyCode == 11 { // b 217 | insertCellBelow() 218 | } else if event.keyCode == 7 { // x 219 | cutCell() 220 | } else if event.keyCode == 9 && flags == .shift { // V 221 | pasteCellAbove() 222 | } else if event.keyCode == 9 { // v 223 | pasteCellBelow() 224 | } else if event.keyCode == 6 { // z 225 | undoCellDeletion() 226 | } else if event.keyCode == 8 { // c 227 | copyCell() 228 | } else if event.keyCode == 46 { // m 229 | setCellType(.markdown) 230 | } else if event.keyCode == 15 { // r 231 | setCellType(.raw) 232 | } else if event.keyCode == 16 { // y 233 | setCellType(.code) 234 | } else if event.keyCode == 1 && flags == .command { // cmd + s 235 | save() 236 | } else if event.keyCode == 5 && flags == .shift { 237 | selectLastCell() 238 | } else if event.keyCode == 2 && flags == .control { // ctrl + d 239 | selectCellBelow(10) 240 | } else if event.keyCode == 32 && flags == .control { // ctrl + u 241 | selectCellAbove(10) 242 | } else if [34, 5, 29].contains(where: { $0 == event.keyCode }) { // i, g, 00 243 | // TODO: Think we should rather keep a keymap. 244 | // Make it a nested dict and somehow use that to determine whether to wait for more keys. 245 | keys.append(event.keyCode) 246 | if let timer { 247 | timer.invalidate() 248 | } 249 | if keys == [34, 34] { // i, i 250 | interruptKernel() 251 | } else if keys == [29, 29] { 252 | restartKernel() 253 | } else if keys == [5, 5] { 254 | selectFirstCell() 255 | } else { 256 | timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { timer in 257 | self.keys = [] 258 | } 259 | return 260 | } 261 | keys = [] 262 | } else { 263 | print(event.keyCode) 264 | super.keyDown(with: event) 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /Jim/Notebook/NotebookViewController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class NotebookViewController: NSViewController { 4 | @IBOutlet var tableView: NotebookTableView! 5 | 6 | var viewModels = [String: NotebookViewModel]() 7 | var viewModel: NotebookViewModel! 8 | 9 | let jupyter = JupyterService.shared 10 | 11 | var notebook: Notebook { viewModel!.notebook } 12 | 13 | let inputLineHeight: CGFloat = { 14 | let string = NSAttributedString(string: "A", attributes: [.font: Theme.shared.monoFont]) 15 | return string.size().height 16 | }() 17 | 18 | let outputLineHeight: CGFloat = { 19 | let string = NSAttributedString(string: "A", attributes: [.font: Theme.shared.monoFont]) 20 | return string.size().height 21 | }() 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | tableView.rowHeight = 20 26 | } 27 | 28 | func notebookSelected(path: String) { 29 | Task { 30 | switch await jupyter.getContent(path, type: Notebook.self) { 31 | case .success(let notebook): 32 | self.viewModel = NotebookViewModel(notebook) 33 | self.viewModels[notebook.path] = self.viewModel 34 | tableView.viewModel = self.viewModel 35 | tableView.reloadData() 36 | switch await jupyter.createSession(name: notebook.name, path: notebook.path) { 37 | case .success(let session): 38 | Task(priority: .background) { jupyter.webSocketTask(session) } 39 | case .failure(let error): 40 | print("Error creating session:", error) 41 | } 42 | case .failure(let error): print("Error getting notebook:", error) 43 | } 44 | } 45 | } 46 | } 47 | 48 | extension NotebookViewController: NSTableViewDataSource { 49 | func numberOfRows(in tableView: NSTableView) -> Int { 50 | viewModel?.cells.count ?? 0 51 | } 52 | } 53 | 54 | extension NotebookViewController: NSTableViewDelegate { 55 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 56 | guard let view = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "notebookCell"), owner: self) as? CellView else { return nil } 57 | let cell = viewModel.cells[row] 58 | let cellViewModel = viewModel.cellViewModel(for: cell) 59 | view.update(with: cellViewModel, tableView: tableView as! NotebookTableView) 60 | return view 61 | } 62 | 63 | func textHeight(_ text: String, lineHeight: CGFloat) -> CGFloat { 64 | CGFloat(text.components(separatedBy: "\n").count) * lineHeight 65 | } 66 | 67 | func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { 68 | // TODO: outputHeight should use TextKit components since it may wrap 69 | let cell = viewModel.cells[row] 70 | let inputVerticalPadding = CGFloat(5) 71 | let inputHeight = 2 * inputVerticalPadding + textHeight(cell.source.value, lineHeight: inputLineHeight) 72 | var outputHeight: CGFloat = 0 73 | if let outputs = cell.outputs { 74 | for output in outputs { 75 | switch output { 76 | case .stream(let output): outputHeight += textHeight(output.text, lineHeight: outputLineHeight) 77 | case .displayData(let output): 78 | if let plainText = output.data.plainText { outputHeight += textHeight(plainText.value, lineHeight: outputLineHeight) } 79 | if let markdownText = output.data.markdownText { outputHeight += textHeight(markdownText.value, lineHeight: outputLineHeight) } 80 | if let htmlText = output.data.markdownText { outputHeight += textHeight(htmlText.value, lineHeight: outputLineHeight) } 81 | if let image = output.data.image { outputHeight += image.value.size.height } 82 | case .executeResult(let output): 83 | if let plainText = output.data.plainText { outputHeight += textHeight(plainText.value, lineHeight: outputLineHeight) } 84 | if let markdownText = output.data.markdownText { outputHeight += textHeight(markdownText.value, lineHeight: outputLineHeight) } 85 | if let htmlText = output.data.markdownText { outputHeight += textHeight(htmlText.value, lineHeight: outputLineHeight) } 86 | if let image = output.data.image { outputHeight += image.value.size.height } 87 | case .error(let output): outputHeight += CGFloat(output.traceback.count)*outputLineHeight 88 | } 89 | } 90 | } 91 | let height = inputHeight + outputHeight 92 | return height 93 | } 94 | 95 | func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { 96 | NotebookTableRowView() 97 | } 98 | 99 | func tableViewSelectionDidChange(_ notification: Notification) { 100 | let windowController = view.window!.windowController as! WindowController 101 | if let title = viewModel.cell(at: tableView.selectedRow)?.cellType.rawValue.capitalized { 102 | windowController.cellTypeComboBox.cell?.title = title 103 | } 104 | } 105 | } 106 | 107 | extension NotebookViewController: NSToolbarItemValidation { 108 | func validateToolbarItem(_ item: NSToolbarItem) -> Bool { 109 | viewModel != nil 110 | } 111 | 112 | @IBAction func insertClicked(_ sender: NSView) { 113 | tableView.insertCellBelow() 114 | } 115 | 116 | @IBAction func cutClicked(_ sender: NSView) { 117 | tableView.cutCell() 118 | } 119 | 120 | @IBAction func copyClicked(_ sender: NSView) { 121 | tableView.copyCell() 122 | } 123 | 124 | @IBAction func pasteClicked(_ sender: NSView) { 125 | tableView.pasteCellBelow() 126 | } 127 | 128 | @IBAction func moveUpClicked(_ sender: NSView) { 129 | tableView.moveCellUp() 130 | } 131 | 132 | @IBAction func moveDownClicked(_ sender: NSView) { 133 | tableView.moveCellDown() 134 | } 135 | 136 | @IBAction func runClicked(_ sender: NSView) { 137 | tableView.runCellSelectBelow() 138 | } 139 | 140 | @IBAction func interruptClicked(_ sender: NSView) { 141 | tableView.interruptKernel() 142 | } 143 | 144 | @IBAction func restartClicked(_ sender: NSView) { 145 | tableView.restartKernel() 146 | } 147 | 148 | @IBAction func restartAndRerunAllClicked(_ sender: NSView) { 149 | // TODO 150 | print("restart kernel and rerun all cells") 151 | } 152 | 153 | @IBAction func setCellTypeClicked(_ sender: NSComboBox) { 154 | // TODO: is there a way for the sender to use an enum directly? 155 | let rawValue = (sender.objectValueOfSelectedItem as! String).lowercased() 156 | let cellType = CellType.init(rawValue: rawValue)! 157 | tableView.setCellType(cellType) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Jim/Notebook/NotebookViewModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Cocoa 3 | 4 | class NotebookViewModel { 5 | var notebook: Notebook 6 | var cellViewModels = [String: CellViewModel]() 7 | 8 | let jupyter = JupyterService.shared 9 | 10 | var cells: [Cell] { notebook.content.cells } 11 | 12 | init(_ notebook: Notebook) { 13 | self.notebook = notebook 14 | } 15 | 16 | func cellViewModel(for cell: Cell) -> CellViewModel { 17 | if let cellViewModel = cellViewModels[cell.id] { 18 | return cellViewModel 19 | } 20 | let cellViewModel = CellViewModel(cell: cell, notebookViewModel: self) 21 | cellViewModels[cell.id] = cellViewModel 22 | return cellViewModel 23 | } 24 | 25 | // MARK: - Cell management 26 | 27 | func insertCell(_ cell: Cell, at row: Int) { 28 | notebook.dirty = true 29 | notebook.content.cells.insert(cell, at: row) 30 | } 31 | 32 | func removeCell(at row: Int) -> Cell { 33 | notebook.dirty = true 34 | return notebook.content.cells.remove(at: row) 35 | } 36 | 37 | func cell(at row: Int) -> Cell? { 38 | if row < 0 || row > notebook.content.cells.count { return nil } 39 | return notebook.content.cells[row] 40 | } 41 | 42 | // MARK: - Jupyter service management 43 | 44 | func getLatestNotebook() async -> Result { 45 | return await jupyter.getContent(notebook.path, type: Notebook.self) 46 | } 47 | 48 | func updateNotebook() async { 49 | switch await jupyter.updateContent(notebook.path, content: notebook) { 50 | case .success(let content): 51 | print("Saved!") // TODO: update UI 52 | notebook.lastModified = content.lastModified 53 | notebook.size = content.size 54 | notebook.dirty = false 55 | case .failure(let error): 56 | print("Failed to save notebook, error:", error) // TODO: show alert 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Jim/Source/SourceScroller.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | protocol SourceScrollerDelegate { 4 | func didMouseDown(_ scroller: SourceScroller) 5 | } 6 | 7 | class SourceScroller: NSScroller { 8 | var delegate: SourceScrollerDelegate? 9 | 10 | override func mouseDown(with event: NSEvent) { 11 | delegate?.didMouseDown(self) 12 | super.mouseDown(with: event) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Jim/Source/SourceTextView.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | class SourceTextView: MinimalTextView { 4 | override var isOpaque: Bool { true } 5 | 6 | override init(layoutManager: NSLayoutManager = NSLayoutManager()) { 7 | super.init() 8 | setup() 9 | } 10 | 11 | required init?(coder: NSCoder) { 12 | super.init(coder: coder) 13 | setup() 14 | } 15 | 16 | private func setup() { 17 | font = Theme.shared.monoFont 18 | drawsBackground = true 19 | backgroundColor = Theme.shared.backgroundColor 20 | allowsUndo = true 21 | setWraps(false, invalidate: false) 22 | } 23 | 24 | // NOTE: We might need this to fix vertical size on table cell reuse 25 | override func invalidateIntrinsicContentSize() { 26 | super.invalidateIntrinsicContentSize() 27 | enclosingScrollView?.invalidateIntrinsicContentSize() 28 | } 29 | 30 | override func becomeFirstResponder() -> Bool { 31 | let sourceView = enclosingScrollView as! SourceView 32 | sourceView.delegate?.didBecomeFirstResponder(sourceView) 33 | return super.becomeFirstResponder() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Jim/Source/SourceView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreGraphics 3 | import AppKit 4 | 5 | protocol SourceViewDelegate: AnyObject { 6 | 7 | func didChangeText(_ sourceView: SourceView) 8 | 9 | func didCommit(_ sourceView: SourceView) 10 | 11 | func previousCell(_ sourceView: SourceView) 12 | 13 | func nextCell(_ sourceView: SourceView) 14 | 15 | func didBecomeFirstResponder(_ sourceView: SourceView) 16 | 17 | func endEditMode(_ sourceView: SourceView) 18 | 19 | func save() 20 | } 21 | 22 | class SourceView: NSScrollView { 23 | var uniqueUndoManager: UndoManager? 24 | let textView: SourceTextView 25 | 26 | override var isOpaque: Bool { true } 27 | 28 | let cornerRadius = 5.0 29 | 30 | weak var delegate: SourceViewDelegate? 31 | 32 | override init(frame: CGRect) { 33 | textView = SourceTextView() 34 | super.init(frame: frame) 35 | setup() 36 | } 37 | 38 | required init?(coder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | private func setup() { 43 | borderType = .noBorder 44 | autohidesScrollers = true 45 | hasVerticalScroller = false 46 | hasHorizontalScroller = true 47 | horizontalScrollElasticity = .automatic 48 | verticalScrollElasticity = .none 49 | drawsBackground = true 50 | backgroundColor = Theme.shared.backgroundColor 51 | 52 | wantsLayer = true 53 | layer?.cornerRadius = cornerRadius 54 | 55 | if let horizontalScroller = horizontalScroller { 56 | let horizontalSourceScroller = SourceScroller(frame: horizontalScroller.frame) 57 | horizontalSourceScroller.delegate = self 58 | self.horizontalScroller = horizontalSourceScroller 59 | } 60 | 61 | textView.delegate = self 62 | 63 | // Needed else textView has layout bugs e.g. it's offset upward by textContainerInset.height 64 | let textViewContainer = NSView() 65 | textViewContainer.addSubview(textView) 66 | documentView = textViewContainer 67 | 68 | textViewContainer.translatesAutoresizingMaskIntoConstraints = false 69 | textView.translatesAutoresizingMaskIntoConstraints = false 70 | NSLayoutConstraint.activate([ 71 | textViewContainer.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), 72 | textViewContainer.trailingAnchor.constraint(greaterThanOrEqualTo: contentView.trailingAnchor), 73 | textViewContainer.topAnchor.constraint(equalTo: contentView.topAnchor), 74 | textViewContainer.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 75 | 76 | textView.leadingAnchor.constraint(equalTo: textViewContainer.leadingAnchor), 77 | textView.trailingAnchor.constraint(equalTo: textViewContainer.trailingAnchor), 78 | textView.topAnchor.constraint(equalTo: textViewContainer.topAnchor), 79 | textView.bottomAnchor.constraint(equalTo: textViewContainer.bottomAnchor), 80 | ]) 81 | textView.setContentHuggingPriority(.required, for: .vertical) 82 | } 83 | 84 | override func scrollWheel(with event: NSEvent) { 85 | if abs(event.deltaX) < abs(event.deltaY) { 86 | super.nextResponder?.scrollWheel(with: event) 87 | } else { 88 | super.scrollWheel(with: event) 89 | } 90 | } 91 | } 92 | 93 | extension SourceView: NSTextViewDelegate { 94 | func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { 95 | guard let event = NSApp.currentEvent else { return false } 96 | let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) 97 | if event.keyCode == 36 && flags == .shift { 98 | delegate?.didCommit(self) 99 | return true 100 | } else if event.keyCode == 125 { 101 | if textView.selectedRange().location == textView.string.count { 102 | delegate?.nextCell(self) 103 | return true 104 | } 105 | } else if event.keyCode == 126 { 106 | if textView.selectedRange().location == 0 { 107 | delegate?.previousCell(self) 108 | return true 109 | } 110 | } else if event.keyCode == 53 { 111 | delegate?.endEditMode(self) 112 | return true 113 | } else if event.keyCode == 1 && flags == .command { 114 | delegate?.save() 115 | return true 116 | } 117 | return false 118 | } 119 | 120 | func undoManager(for view: NSTextView) -> UndoManager? { 121 | uniqueUndoManager 122 | } 123 | 124 | public func textDidChange(_ notification: Notification) { 125 | guard let textView = notification.object as? SourceTextView, textView == self.textView else { return } 126 | delegate?.didChangeText(self) 127 | } 128 | } 129 | 130 | extension SourceView: SourceScrollerDelegate { 131 | func didMouseDown(_ scroller: SourceScroller) { 132 | delegate?.didBecomeFirstResponder(self) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Jim/Theme.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Foundation 3 | 4 | struct Theme { 5 | static let shared = Theme() 6 | let monoFont = NSFont.monospacedSystemFont(ofSize: 14, weight: .regular) 7 | let font = NSFont.systemFont(ofSize: 14) 8 | let backgroundColor = NSColor(red: 0.95, green: 0.95, blue: 0.95, alpha: 1.0) 9 | 10 | let sidebarTintColor = NSColor(red: 0.494, green: 0.506, blue: 0.514, alpha: 1.0) 11 | } 12 | -------------------------------------------------------------------------------- /Jim/Utils/Base64Image.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct Base64Image: Codable, Hashable { 4 | let text: String 5 | let value: NSImage 6 | 7 | func hash(into hasher: inout Hasher) { 8 | hasher.combine(text) 9 | } 10 | 11 | init(from decoder: Decoder) throws { 12 | let container = try decoder.singleValueContainer() 13 | self.text = try container.decode(String.self) 14 | guard let data = Data(base64Encoded: self.text.trimmingCharacters(in: .whitespacesAndNewlines)), 15 | let nsImage = NSImage(data: data) else { 16 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid Base64Image") 17 | } 18 | self.value = nsImage 19 | } 20 | 21 | func encode(to encoder: Encoder) throws { 22 | var container = encoder.singleValueContainer() 23 | try container.encode(text) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Jim/Utils/Debug.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | func measure(_ label: String, callback: (() -> ())) { 4 | let t0 = DispatchTime.now() 5 | callback() 6 | let t1 = DispatchTime.now() 7 | let dt = CGFloat(t1.uptimeNanoseconds - t0.uptimeNanoseconds) / 1_000_000 8 | print("Took \(dt) ms to do \(label)") 9 | } 10 | 11 | func printResponderChain(from responder: NSResponder?) { 12 | var responder = responder 13 | while let r = responder { 14 | print(r) 15 | responder = r.nextResponder 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Jim/Utils/StringOrArray.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct StringOrArray: Codable, Hashable, Equatable { 4 | var value: String 5 | 6 | init(_ value: String) { 7 | self.value = value 8 | } 9 | 10 | init(from decoder: Decoder) throws { 11 | let container = try decoder.singleValueContainer() 12 | if let array = try? container.decode([String].self) { 13 | self.value = array.joined() 14 | } else if let string = try? container.decode(String.self) { 15 | self.value = string 16 | } else { 17 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "StringOrArray value cannot be decoded") 18 | } 19 | } 20 | 21 | enum CodingKeys: CodingKey { 22 | case value 23 | } 24 | 25 | func encode(to encoder: Encoder) throws { 26 | var container = encoder.singleValueContainer() 27 | try container.encode(self.value) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jim 2 | 3 | Jim (Jupyter in macOS) is a simple, beautiful Jupyter notebook editor for macOS. Think [Bear](https://bear.app/), for [Jupyter](https://jupyter.org/). 4 | 5 | **NB: Jim is in pre-alpha and therefore not yet very reliable. You may lose work due to an unknown bug. Use Jim at your own risk.** 6 | 7 | https://github.com/seeM/Jim/assets/559360/332cea34-b32e-4bfb-a27f-b22e3773f4bb 8 | 9 | 10 | 11 | ## Contributing 12 | 13 | All contributions to Jim are welcome! Here's how you can get Jim up and running on your computer. 14 | 15 | ### Get the project source 16 | 17 | First ensure that you have installed [XCode](https://apps.apple.com/us/app/xcode/id497799835) from the app store. 18 | 19 | Then clone this repo: 20 | 21 | ``` sh 22 | git clone git@github.com:seeM/Jim.git 23 | ``` 24 | 25 | Open the project, either in XCode directly or via the command line: 26 | 27 | ``` sh 28 | open Jim/Jim.xcodeproj 29 | ``` 30 | 31 | ### Start a Jupyter Server 32 | 33 | Jim connects to an existing [Jupyter Server](https://jupyter-server.readthedocs.io/en/latest/) which must be started either in another process or remotely. Install Jupyter Server if needed with: 34 | 35 | ``` sh 36 | pip install jupyter-server 37 | ``` 38 | 39 | Then start the server: 40 | 41 | ``` sh 42 | jupyter server 43 | ``` 44 | 45 | Note the server URL and token displayed in the logs, which are needed to login with Jim. For example, if your Jupyter Server produced the following logs: 46 | 47 | ``` 48 | [C 2023-05-27 15:01:55.088 ServerApp] 49 | 50 | To access the server, open this file in a browser: 51 | file:///Users/seem/Library/Jupyter/runtime/jpserver-5988-open.html 52 | Or copy and paste one of these URLs: 53 | http://localhost:8888/?token=6dc6eebb717ed09c243da57a89bee6a30dba217fbd43ec15 54 | http://127.0.0.1:8888/?token=6dc6eebb717ed09c243da57a89bee6a30dba217fbd43ec15 55 | ``` 56 | 57 | The corresponding login URL would be `http://localhost:8888/` and the token would be `6dc6eebb717ed09c243da57a89bee6a30dba217fbd43ec15` 58 | 59 | ***Note: Jim doesn't yet have a login screen (see [issue #15](https://github.com/seeM/Jim/issues/15#issue-1676257828)). The login URL and token are currently hardcoded. You can use the command `jupyter server --port 8999 --ServerApp.token=testtoken123` to start a development server matching those hardcoded values, or change the values ([here](https://github.com/seeM/Jim/blob/7e14cd8e70df1057c6888e9126c3524066a41db5/Jim/Main/SidebarViewController.swift#L21)) to match your server's configuration.*** 60 | 61 | ### Start Jim 62 | 63 | Run Jim by clicking on the play button on the top left of the XCode toolbar or by pressing ⌘R. -------------------------------------------------------------------------------- /Whats New/2023-03-29-whats-new.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "32486572-35F3-4470-ADAE-F8D00AA5732A", 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# ✨ What's new — March 23rd" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "id": "F524F659-8A74-4523-B2EB-A140A34B545D", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "## 🚀 Markdown\n", 19 | "\n", 20 | "**Bold**, _italics_, `code`.\n", 21 | "\n", 22 | "```\n", 23 | "# code blocks\n", 24 | "```\n", 25 | "\n", 26 | "Horizontal rules:\n", 27 | "\n", 28 | "---\n", 29 | "\n", 30 | "Quotes:\n", 31 | "\n", 32 | "> Yay\n", 33 | "\n", 34 | "And lists (see below)!" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "id": "6517C658-355D-4156-9282-772DC727D56D", 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "## 🛠️ Toolbar\n", 44 | "\n", 45 | "- Create cell\n", 46 | "- Cut cell\n", 47 | "- Copy cell\n", 48 | "- Paste cell\n", 49 | "- Move cell up\n", 50 | "- Move cell down\n", 51 | "- Run cell\n", 52 | "- Stop running cell\n", 53 | "- Restart kernel\n", 54 | "- Run all cells\n", 55 | "- Set cell type" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "id": "55349766-D6DA-4BD1-9983-7C5CC13922A3", 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "## 🎨 UI\n", 65 | "\n", 66 | "- Improved tons of small interactions & styling!" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "id": "6FE8FAD1-FA90-4618-BB29-EE6EC7530D99", 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "## ✅ Misc.\n", 76 | "\n", 77 | "- Added support for key sequences, like `i i` to interrupt kernel\n", 78 | "- Fixed delay before entering edit mode" 79 | ] 80 | } 81 | ], 82 | "metadata": { 83 | "kernelspec": { 84 | "display_name": "python3", 85 | "language": "python", 86 | "name": "python3" 87 | } 88 | }, 89 | "nbformat": 4, 90 | "nbformat_minor": 5 91 | } 92 | --------------------------------------------------------------------------------