mirror of
https://github.com/gravitational/teleport
synced 2024-10-20 09:13:39 +00:00
Connect: Add theme configuration (#27788)
* Add a config item for the theme and adjust Electron's `nativeTheme` based on that * Listen to theme changes and update the app accordingly * React to theme changes in teleterm stories * Rename channel * Return boolean from handlers * Do not mock the whole app context in storybook * Fix linting issue * Fix typo
This commit is contained in:
parent
e23e6b6082
commit
95e6482043
|
@ -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)
|
||||
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
@ -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) => {
|
||||
|
|
|
@ -68,6 +68,14 @@ export class MockMainProcessClient implements MainProcessClient {
|
|||
async openConfigFile() {
|
||||
return '';
|
||||
}
|
||||
|
||||
shouldUseDarkColors() {
|
||||
return true;
|
||||
}
|
||||
|
||||
subscribeToNativeThemeUpdate() {
|
||||
return { cleanup: () => undefined };
|
||||
}
|
||||
}
|
||||
|
||||
export const makeRuntimeSettings = (
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
ipcMain,
|
||||
Menu,
|
||||
MenuItemConstructorOptions,
|
||||
nativeTheme,
|
||||
shell,
|
||||
} from 'electron';
|
||||
import { wait } from 'shared/utils/wait';
|
||||
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
|
|
@ -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.'),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -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.
|
||||
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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 { GlobalStyle } from './globals';
|
||||
import theme from './theme';
|
||||
import { useAppContext } from 'teleterm/ui/appContextProvider';
|
||||
|
||||
export const ThemeProvider: React.FC = props => (
|
||||
<StyledThemeProvider theme={theme}>
|
||||
<StyleSheetManager disableVendorPrefixes>
|
||||
<React.Fragment>
|
||||
<GlobalStyle />
|
||||
{props.children}
|
||||
</React.Fragment>
|
||||
</StyleSheetManager>
|
||||
</StyledThemeProvider>
|
||||
);
|
||||
import { GlobalStyle } from './globals';
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -23,7 +23,7 @@ import { lighten } from 'design/theme/utils/colorManipulator';
|
|||
|
||||
const sansSerif = 'system-ui';
|
||||
|
||||
const darkTheme = {
|
||||
export const darkTheme = {
|
||||
...designDarkTheme,
|
||||
colors: {
|
||||
...designDarkTheme.colors,
|
||||
|
@ -43,8 +43,7 @@ const darkTheme = {
|
|||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const lightTheme = {
|
||||
export const lightTheme = {
|
||||
...designLightTheme,
|
||||
font: sansSerif,
|
||||
fonts: {
|
||||
|
@ -52,5 +51,3 @@ const lightTheme = {
|
|||
mono: fonts.mono,
|
||||
},
|
||||
};
|
||||
|
||||
export default darkTheme;
|
||||
|
|
Loading…
Reference in a new issue