Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions web/.storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ import { darkTheme, lightTheme } from './../packages/design/src/theme';
import DefaultThemeProvider from '../packages/design/src/ThemeProvider';
import Box from './../packages/design/src/Box';
import '../packages/teleport/src/lib/polyfillRandomUuid';
import { ThemeProvider as TeletermThemeProvider } from './../packages/teleterm/src/ui/ThemeProvider';
import { theme as TeletermTheme } from './../packages/teleterm/src/ui/ThemeProvider/theme';
import { StaticThemeProvider as TeletermThemeProvider } from './../packages/teleterm/src/ui/ThemeProvider';
import {
darkTheme as teletermDarkTheme,
lightTheme as teletermLightTheme,
} from './../packages/teleterm/src/ui/ThemeProvider/theme';
import { handlersTeleport } from './../packages/teleport/src/mocks/handlers';

// Checks we are running non-node environment (browser)
Expand All @@ -41,7 +44,10 @@ const ThemeDecorator = (storyFn, meta) => {

if (meta.title.startsWith('Teleterm/')) {
ThemeProvider = TeletermThemeProvider;
theme = TeletermTheme;
theme =
meta.globals.theme === 'Dark Theme'
? teletermDarkTheme
: teletermLightTheme;
} else {
ThemeProvider = DefaultThemeProvider;
theme = meta.globals.theme === 'Dark Theme' ? darkTheme : lightTheme;
Expand Down
4 changes: 3 additions & 1 deletion web/packages/teleterm/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { spawn } from 'child_process';

import path from 'path';

import { app, globalShortcut, shell } from 'electron';
import { app, globalShortcut, shell, nativeTheme } from 'electron';

import MainProcess from 'teleterm/mainProcess';
import { getRuntimeSettings } from 'teleterm/mainProcess/runtimeSettings';
Expand Down Expand Up @@ -59,6 +59,8 @@ async function initializeApp(): Promise<void> {
jsonSchemaFile: configJsonSchemaFileStorage,
platform: settings.platform,
});

nativeTheme.themeSource = configService.get('theme').value;
const windowsManager = new WindowsManager(appStateFileStorage, settings);

process.on('uncaughtException', (error, origin) => {
Expand Down
8 changes: 8 additions & 0 deletions web/packages/teleterm/src/mainProcess/fixtures/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ export class MockMainProcessClient implements MainProcessClient {
async openConfigFile() {
return '';
}

shouldUseDarkColors() {
return true;
}

subscribeToNativeThemeUpdate() {
return { cleanup: () => undefined };
}
}

export const makeRuntimeSettings = (
Expand Down
5 changes: 5 additions & 0 deletions web/packages/teleterm/src/mainProcess/mainProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
ipcMain,
Menu,
MenuItemConstructorOptions,
nativeTheme,
shell,
} from 'electron';
import { wait } from 'shared/utils/wait';
Expand Down Expand Up @@ -193,6 +194,10 @@ export default class MainProcess {
event.returnValue = this.settings;
});

ipcMain.on('main-process-should-use-dark-colors', event => {
event.returnValue = nativeTheme.shouldUseDarkColors;
});

ipcMain.handle('main-process-get-resolved-child-process-addresses', () => {
return this.resolvedChildProcessAddresses;
});
Expand Down
14 changes: 13 additions & 1 deletion web/packages/teleterm/src/mainProcess/mainProcessClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,20 @@ export default function createMainProcessClient(): MainProcessClient {
removeTshSymlinkMacOs() {
return ipcRenderer.invoke('main-process-remove-tsh-symlink-macos');
},
openConfigFile(): Promise<string> {
openConfigFile() {
return ipcRenderer.invoke('main-process-open-config-file');
},
shouldUseDarkColors() {
return ipcRenderer.sendSync('main-process-should-use-dark-colors');
},
subscribeToNativeThemeUpdate: listener => {
const onThemeChange = (_, value: { shouldUseDarkColors: boolean }) =>
listener(value);
const channel = 'main-process-native-theme-update';
ipcRenderer.addListener(channel, onThemeChange);
return {
cleanup: () => ipcRenderer.removeListener(channel, onThemeChange),
};
},
};
}
7 changes: 7 additions & 0 deletions web/packages/teleterm/src/mainProcess/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ export type MainProcessClient = {

/** Opens config file and returns a path to it. */
openConfigFile(): Promise<string>;
shouldUseDarkColors(): boolean;
/** Subscribes to updates of the native theme. Returns a cleanup function. */
subscribeToNativeThemeUpdate: (
listener: (value: { shouldUseDarkColors: boolean }) => void
) => {
cleanup: () => void;
};
};

export type ChildProcessAddresses = {
Expand Down
22 changes: 19 additions & 3 deletions web/packages/teleterm/src/mainProcess/windowsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,18 @@

import path from 'path';

import { app, BrowserWindow, Menu, Rectangle, screen } from 'electron';
import {
app,
BrowserWindow,
Menu,
Rectangle,
screen,
nativeTheme,
} from 'electron';

import { FileStorage } from 'teleterm/services/fileStorage';
import { RuntimeSettings } from 'teleterm/mainProcess/types';
import theme from 'teleterm/ui/ThemeProvider/theme';
import { darkTheme, lightTheme } from 'teleterm/ui/ThemeProvider/theme';

type WindowState = Rectangle;

Expand All @@ -47,13 +54,16 @@ export class WindowsManager {
}

createWindow(): void {
const activeTheme = nativeTheme.shouldUseDarkColors
? darkTheme
: lightTheme;
const windowState = this.getWindowState();
const window = new BrowserWindow({
x: windowState.x,
y: windowState.y,
width: windowState.width,
height: windowState.height,
backgroundColor: theme.colors.levels.sunken,
backgroundColor: activeTheme.colors.levels.sunken,
minWidth: 400,
minHeight: 300,
show: false,
Expand Down Expand Up @@ -88,6 +98,12 @@ export class WindowsManager {
this.popupUniversalContextMenu(window, props);
});

nativeTheme.on('updated', () => {
window.webContents.send('main-process-native-theme-update', {
shouldUseDarkColors: nativeTheme.shouldUseDarkColors,
});
});

window.webContents.session.setPermissionRequestHandler(
(webContents, permission, callback) => {
// deny all permissions requests, we currently do not require any
Expand Down
4 changes: 4 additions & 0 deletions web/packages/teleterm/src/services/config/appConfigSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ export const createAppConfigSchema = (platform: Platform) => {
.max(256)
.default(15)
.describe('Font size for the terminal.'),
theme: z
.enum(['light', 'dark', 'system'])
.default('system')
.describe('Color theme for the app.'),
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ limitations under the License.
*/

import React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import styled, { useTheme } from 'styled-components';
import { Box, Flex } from 'design';
import { debounce } from 'shared/utils/highbar';
import {
Expand Down Expand Up @@ -54,6 +54,7 @@ export function Terminal(props: TerminalProps) {
const [startPtyProcessAttempt, setStartPtyProcessAttempt] = useState<
Attempt<void>
>(makeEmptyAttempt());
const theme = useTheme();

useEffect(() => {
const removeOnStartErrorListener = props.ptyProcess.onStartError(
Expand All @@ -69,6 +70,7 @@ export function Terminal(props: TerminalProps) {
const ctrl = new XTermCtrl(props.ptyProcess, {
el: refElement.current,
fontSize: props.fontSize,
theme: theme.colors.terminal,
});

// Start the PTY process.
Expand Down Expand Up @@ -103,6 +105,12 @@ export function Terminal(props: TerminalProps) {
refCtrl.current.requestResize();
}, [props.visible]);

useEffect(() => {
if (refCtrl.current) {
refCtrl.current.term.options.theme = theme.colors.terminal;
}
}, [theme]);

return (
<Flex
flexDirection="column"
Expand Down
28 changes: 3 additions & 25 deletions web/packages/teleterm/src/ui/DocumentTerminal/Terminal/ctrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ limitations under the License.
*/

import 'xterm/css/xterm.css';
import { IDisposable, Terminal } from 'xterm';
import { IDisposable, ITheme, Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { debounce } from 'shared/utils/highbar';

import { IPtyProcess } from 'teleterm/sharedProcess/ptyHost';
import Logger from 'teleterm/logger';
import theme from 'teleterm/ui/ThemeProvider/theme';

const WINDOW_RESIZE_DEBOUNCE_DELAY = 200;

type Options = {
el: HTMLElement;
fontSize: number;
theme: ITheme;
};

export default class TtyTerminal {
Expand Down Expand Up @@ -62,29 +62,7 @@ export default class TtyTerminal {
fontSize: this.options.fontSize,
scrollback: 5000,
minimumContrastRatio: 4.5, // minimum for WCAG AA compliance
theme: {
foreground: theme.colors.terminal.foreground,
background: theme.colors.terminal.background,
selectionBackground: theme.colors.terminal.selectionBackground,
cursor: theme.colors.terminal.cursor,
cursorAccent: theme.colors.terminal.cursorAccent,
red: theme.colors.terminal.red,
green: theme.colors.terminal.green,
yellow: theme.colors.terminal.yellow,
blue: theme.colors.terminal.blue,
magenta: theme.colors.terminal.magenta,
cyan: theme.colors.terminal.cyan,
brightWhite: theme.colors.terminal.brightWhite,
white: theme.colors.terminal.white,
brightBlack: theme.colors.terminal.brightBlack,
black: theme.colors.terminal.black,
brightRed: theme.colors.terminal.brightRed,
brightGreen: theme.colors.terminal.brightGreen,
brightYellow: theme.colors.terminal.brightYellow,
brightBlue: theme.colors.terminal.brightBlue,
brightMagenta: theme.colors.terminal.brightMagenta,
brightCyan: theme.colors.terminal.brightCyan,
},
theme: this.options.theme,
windowOptions: {
setWinSizeChars: true,
},
Expand Down
66 changes: 53 additions & 13 deletions web/packages/teleterm/src/ui/ThemeProvider/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,62 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';
import React, { useEffect, useState } from 'react';
import {
ThemeProvider as StyledThemeProvider,
StyleSheetManager,
} from 'styled-components';

import { useAppContext } from 'teleterm/ui/appContextProvider';

import { GlobalStyle } from './globals';
import theme from './theme';

export const ThemeProvider: React.FC = props => (
<StyledThemeProvider theme={theme}>
<StyleSheetManager disableVendorPrefixes>
<React.Fragment>
<GlobalStyle />
{props.children}
</React.Fragment>
</StyleSheetManager>
</StyledThemeProvider>
);
import { darkTheme, lightTheme } from './theme';

export const ThemeProvider = (props: React.PropsWithChildren<unknown>) => {
// Listening to Electron's nativeTheme.on('updated') is a workaround.
// The renderer should be able to get the current theme via "prefers-color-scheme" media query.
// Unfortunately, it does not work correctly on Ubuntu where the query from above always returns the old value
// (for example, when the app was launched in a dark mode, it always returns 'dark'
// ignoring that the system theme is now 'light').
// Related Electron issue: https://github.com/electron/electron/issues/21427#issuecomment-589796481,
// Related Chromium issue: https://bugs.chromium.org/p/chromium/issues/detail?id=998903
//
// Additional issue is that nativeTheme does not return correct values at all on Fedora:
// https://github.com/electron/electron/issues/33635#issuecomment-1502215450
const ctx = useAppContext();
const [activeTheme, setActiveTheme] = useState(() =>
ctx.mainProcessClient.shouldUseDarkColors() ? darkTheme : lightTheme
);

useEffect(() => {
const { cleanup } = ctx.mainProcessClient.subscribeToNativeThemeUpdate(
({ shouldUseDarkColors }) => {
setActiveTheme(shouldUseDarkColors ? darkTheme : lightTheme);
}
);

return cleanup;
}, [ctx.mainProcessClient]);

return (
<StaticThemeProvider theme={activeTheme}>
{props.children}
</StaticThemeProvider>
);
};

/** Uses a theme from a prop. Useful in storybook. */
export const StaticThemeProvider = (
props: React.PropsWithChildren<{ theme?: unknown }>
) => {
return (
<StyledThemeProvider theme={props.theme}>
<StyleSheetManager disableVendorPrefixes>
<React.Fragment>
<GlobalStyle />
{props.children}
</React.Fragment>
</StyleSheetManager>
</StyledThemeProvider>
);
};
2 changes: 1 addition & 1 deletion web/packages/teleterm/src/ui/ThemeProvider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

export { ThemeProvider } from './ThemeProvider';
export * from './ThemeProvider';
7 changes: 2 additions & 5 deletions web/packages/teleterm/src/ui/ThemeProvider/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { lighten } from 'design/theme/utils/colorManipulator';

const sansSerif = 'system-ui';

const darkTheme = {
export const darkTheme = {
...designDarkTheme,
colors: {
...designDarkTheme.colors,
Expand All @@ -43,14 +43,11 @@ const darkTheme = {
},
};

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const lightTheme = {
export const lightTheme = {
...designLightTheme,
font: sansSerif,
fonts: {
sansSerif,
mono: fonts.mono,
},
};

export default darkTheme;