index.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { ISettingRegistry } from '@jupyterlab/coreutils';
  4. import {
  5. ILayoutRestorer,
  6. JupyterFrontEnd,
  7. JupyterFrontEndPlugin
  8. } from '@jupyterlab/application';
  9. import {
  10. ICommandPalette,
  11. InstanceTracker,
  12. IThemeManager,
  13. MainAreaWidget
  14. } from '@jupyterlab/apputils';
  15. import { ILauncher } from '@jupyterlab/launcher';
  16. import { IMainMenu } from '@jupyterlab/mainmenu';
  17. import {
  18. ITerminalTracker,
  19. ITerminal
  20. } from '@jupyterlab/terminal/lib/constants';
  21. // Name-only import so as to not trigger inclusion in main bundle
  22. import * as WidgetModuleType from '@jupyterlab/terminal/lib/widget';
  23. import { Menu } from '@phosphor/widgets';
  24. /**
  25. * The command IDs used by the terminal plugin.
  26. */
  27. namespace CommandIDs {
  28. export const createNew = 'terminal:create-new';
  29. export const open = 'terminal:open';
  30. export const refresh = 'terminal:refresh';
  31. export const increaseFont = 'terminal:increase-font';
  32. export const decreaseFont = 'terminal:decrease-font';
  33. export const setTheme = 'terminal:set-theme';
  34. }
  35. /**
  36. * The class name for the terminal icon in the default theme.
  37. */
  38. const TERMINAL_ICON_CLASS = 'jp-TerminalIcon';
  39. /**
  40. * The default terminal extension.
  41. */
  42. const plugin: JupyterFrontEndPlugin<ITerminalTracker> = {
  43. activate,
  44. id: '@jupyterlab/terminal-extension:plugin',
  45. provides: ITerminalTracker,
  46. requires: [ISettingRegistry],
  47. optional: [
  48. ICommandPalette,
  49. ILauncher,
  50. ILayoutRestorer,
  51. IMainMenu,
  52. IThemeManager
  53. ],
  54. autoStart: true
  55. };
  56. /**
  57. * Export the plugin as default.
  58. */
  59. export default plugin;
  60. /**
  61. * Activate the terminal plugin.
  62. */
  63. function activate(
  64. app: JupyterFrontEnd,
  65. settingRegistry: ISettingRegistry,
  66. palette: ICommandPalette | null,
  67. launcher: ILauncher | null,
  68. restorer: ILayoutRestorer | null,
  69. mainMenu: IMainMenu | null,
  70. themeManager: IThemeManager
  71. ): ITerminalTracker {
  72. const { serviceManager, commands } = app;
  73. const category = 'Terminal';
  74. const namespace = 'terminal';
  75. const tracker = new InstanceTracker<MainAreaWidget<ITerminal.ITerminal>>({
  76. namespace
  77. });
  78. // Bail if there are no terminals available.
  79. if (!serviceManager.terminals.isAvailable()) {
  80. console.log(
  81. 'Disabling terminals plugin because they are not available on the server'
  82. );
  83. return tracker;
  84. }
  85. // Handle state restoration.
  86. if (restorer) {
  87. restorer.restore(tracker, {
  88. command: CommandIDs.createNew,
  89. args: widget => ({ name: widget.content.session.name }),
  90. name: widget => widget.content.session && widget.content.session.name
  91. });
  92. }
  93. // The terminal options from the setting editor.
  94. let options: Partial<ITerminal.IOptions>;
  95. /**
  96. * Update the option values.
  97. */
  98. function updateOptions(settings: ISettingRegistry.ISettings): void {
  99. options = settings.composite as Partial<ITerminal.IOptions>;
  100. Object.keys(options).forEach((key: keyof ITerminal.IOptions) => {
  101. ITerminal.defaultOptions[key] = options[key];
  102. });
  103. }
  104. /**
  105. * Update terminal
  106. */
  107. function updateTerminal(widget: MainAreaWidget<ITerminal.ITerminal>): void {
  108. const terminal = widget.content;
  109. if (!terminal) {
  110. return;
  111. }
  112. Object.keys(options).forEach((key: keyof ITerminal.IOptions) => {
  113. terminal.setOption(key, options[key]);
  114. });
  115. }
  116. /**
  117. * Update the settings of the current tracker instances.
  118. */
  119. function updateTracker(): void {
  120. tracker.forEach(widget => updateTerminal(widget));
  121. }
  122. // Fetch the initial state of the settings.
  123. settingRegistry
  124. .load(plugin.id)
  125. .then(settings => {
  126. updateOptions(settings);
  127. updateTracker();
  128. settings.changed.connect(() => {
  129. updateOptions(settings);
  130. updateTracker();
  131. });
  132. })
  133. .catch(Private.showErrorMessage);
  134. // Subscribe to changes in theme.
  135. themeManager.themeChanged.connect((sender, args) => {
  136. tracker.forEach(widget => {
  137. const terminal = widget.content;
  138. if (terminal.getOption('theme') === 'inherit') {
  139. terminal.setOption('theme', 'inherit');
  140. }
  141. });
  142. });
  143. addCommands(app, tracker, settingRegistry);
  144. if (mainMenu) {
  145. // Add "Terminal Theme" menu below "JupyterLab Themes" menu.
  146. const themeMenu = new Menu({ commands });
  147. themeMenu.title.label = 'Terminal Theme';
  148. themeMenu.addItem({
  149. command: CommandIDs.setTheme,
  150. args: { theme: 'inherit', isPalette: false }
  151. });
  152. themeMenu.addItem({
  153. command: CommandIDs.setTheme,
  154. args: { theme: 'light', isPalette: false }
  155. });
  156. themeMenu.addItem({
  157. command: CommandIDs.setTheme,
  158. args: { theme: 'dark', isPalette: false }
  159. });
  160. mainMenu.settingsMenu.addGroup(
  161. [{ type: 'submenu', submenu: themeMenu }],
  162. 2
  163. );
  164. // Add some commands to the "View" menu.
  165. mainMenu.settingsMenu.addGroup(
  166. [
  167. { command: CommandIDs.increaseFont },
  168. { command: CommandIDs.decreaseFont },
  169. { type: 'submenu', submenu: themeMenu }
  170. ],
  171. 40
  172. );
  173. // Add terminal creation to the file menu.
  174. mainMenu.fileMenu.newMenu.addGroup([{ command: CommandIDs.createNew }], 20);
  175. }
  176. if (palette) {
  177. // Add command palette items.
  178. [
  179. CommandIDs.createNew,
  180. CommandIDs.refresh,
  181. CommandIDs.increaseFont,
  182. CommandIDs.decreaseFont
  183. ].forEach(command => {
  184. palette.addItem({ command, category, args: { isPalette: true } });
  185. });
  186. palette.addItem({
  187. command: CommandIDs.setTheme,
  188. category,
  189. args: { theme: 'inherit', isPalette: true }
  190. });
  191. palette.addItem({
  192. command: CommandIDs.setTheme,
  193. category,
  194. args: { theme: 'light', isPalette: true }
  195. });
  196. palette.addItem({
  197. command: CommandIDs.setTheme,
  198. category,
  199. args: { theme: 'dark', isPalette: true }
  200. });
  201. }
  202. // Add a launcher item if the launcher is available.
  203. if (launcher) {
  204. launcher.add({
  205. command: CommandIDs.createNew,
  206. category: 'Other',
  207. rank: 0
  208. });
  209. }
  210. app.contextMenu.addItem({
  211. command: CommandIDs.refresh,
  212. selector: '.jp-Terminal',
  213. rank: 1
  214. });
  215. return tracker;
  216. }
  217. /**
  218. * Add the commands for the terminal.
  219. */
  220. export function addCommands(
  221. app: JupyterFrontEnd,
  222. tracker: InstanceTracker<MainAreaWidget<ITerminal.ITerminal>>,
  223. settingRegistry: ISettingRegistry
  224. ) {
  225. const { commands, serviceManager } = app;
  226. // Add terminal commands.
  227. commands.addCommand(CommandIDs.createNew, {
  228. label: args => (args['isPalette'] ? 'New Terminal' : 'Terminal'),
  229. caption: 'Start a new terminal session',
  230. iconClass: args => (args['isPalette'] ? '' : TERMINAL_ICON_CLASS),
  231. execute: async args => {
  232. // wait for the widget to lazy load
  233. let Terminal: typeof WidgetModuleType.Terminal;
  234. try {
  235. Terminal = (await Private.ensureWidget()).Terminal;
  236. } catch (err) {
  237. Private.showErrorMessage(err);
  238. }
  239. const name = args['name'] as string;
  240. const term = new Terminal();
  241. term.title.icon = TERMINAL_ICON_CLASS;
  242. term.title.label = '...';
  243. let main = new MainAreaWidget({ content: term });
  244. app.shell.add(main);
  245. try {
  246. term.session = await (name
  247. ? serviceManager.terminals
  248. .connectTo(name)
  249. .catch(() => serviceManager.terminals.startNew())
  250. : serviceManager.terminals.startNew());
  251. void tracker.add(main);
  252. app.shell.activateById(main.id);
  253. return main;
  254. } catch {
  255. term.dispose();
  256. }
  257. }
  258. });
  259. commands.addCommand(CommandIDs.open, {
  260. execute: args => {
  261. const name = args['name'] as string;
  262. // Check for a running terminal with the given name.
  263. const widget = tracker.find(value => {
  264. let content = value.content;
  265. return (content.session && content.session.name === name) || false;
  266. });
  267. if (widget) {
  268. app.shell.activateById(widget.id);
  269. } else {
  270. // Otherwise, create a new terminal with a given name.
  271. return commands.execute(CommandIDs.createNew, { name });
  272. }
  273. }
  274. });
  275. commands.addCommand(CommandIDs.refresh, {
  276. label: 'Refresh Terminal',
  277. caption: 'Refresh the current terminal session',
  278. execute: async () => {
  279. let current = tracker.currentWidget;
  280. if (!current) {
  281. return;
  282. }
  283. app.shell.activateById(current.id);
  284. try {
  285. await current.content.refresh();
  286. if (current) {
  287. current.content.activate();
  288. }
  289. } catch (err) {
  290. Private.showErrorMessage(err);
  291. }
  292. },
  293. isEnabled: () => tracker.currentWidget !== null
  294. });
  295. commands.addCommand(CommandIDs.increaseFont, {
  296. label: 'Increase Terminal Font Size',
  297. execute: async () => {
  298. let { fontSize } = ITerminal.defaultOptions;
  299. if (fontSize < 72) {
  300. try {
  301. await settingRegistry.set(plugin.id, 'fontSize', fontSize + 1);
  302. } catch (err) {
  303. Private.showErrorMessage(err);
  304. }
  305. }
  306. }
  307. });
  308. commands.addCommand(CommandIDs.decreaseFont, {
  309. label: 'Decrease Terminal Font Size',
  310. execute: async () => {
  311. let { fontSize } = ITerminal.defaultOptions;
  312. if (fontSize > 9) {
  313. try {
  314. await settingRegistry.set(plugin.id, 'fontSize', fontSize - 1);
  315. } catch (err) {
  316. Private.showErrorMessage(err);
  317. }
  318. }
  319. }
  320. });
  321. commands.addCommand(CommandIDs.setTheme, {
  322. label: args => {
  323. const theme = args['theme'] as string;
  324. const displayName = theme[0].toUpperCase() + theme.substring(1);
  325. return args['isPalette']
  326. ? `Use ${displayName} Terminal Theme`
  327. : displayName;
  328. },
  329. caption: 'Set the terminal theme',
  330. isToggled: args => args['theme'] === ITerminal.defaultOptions.theme,
  331. execute: async args => {
  332. const theme = args['theme'] as ITerminal.Theme;
  333. try {
  334. await settingRegistry.set(plugin.id, 'theme', theme);
  335. commands.notifyCommandChanged(CommandIDs.setTheme);
  336. } catch (err) {
  337. Private.showErrorMessage(err);
  338. }
  339. }
  340. });
  341. }
  342. /**
  343. * A namespace for private data.
  344. */
  345. namespace Private {
  346. /**
  347. * A Promise for the initial load of the terminal widget.
  348. */
  349. export let widgetReady: Promise<typeof WidgetModuleType>;
  350. /**
  351. * Lazy-load the widget (and xterm library and addons)
  352. */
  353. export function ensureWidget(): Promise<typeof WidgetModuleType> {
  354. if (widgetReady) {
  355. return widgetReady;
  356. }
  357. widgetReady = import('@jupyterlab/terminal/lib/widget');
  358. return widgetReady;
  359. }
  360. /**
  361. * Utility function for consistent error reporting
  362. */
  363. export function showErrorMessage(error: Error): void {
  364. console.error(`Failed to configure ${plugin.id}: ${error.message}`);
  365. }
  366. }