blob: a9743c786ab2864770a6d59ee216827374c8471f [file] [log] [blame]
// Copyright 2020 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Exports an element: terminal-settings-theme.
*
* @suppress {moduleLoad}
*/
import {lib} from '../../libdot/index.js';
import {hterm} from '../../hterm/index.js';
import {LitElement, css, html, unsafeCSS} from './lit.js';
import {DEFAULT_THEME, DEFAULT_ANSI_COLORS, DEFAULT_BACKGROUND_COLOR,
DEFAULT_CURSOR_COLOR, DEFAULT_FOREGROUND_COLOR} from './terminal_common.js';
import './terminal_button.js';
import './terminal_dialog.js';
/** @typedef {!Object<string, *>} */
let ThemeVariation;
/** @typedef {!Object<string, !ThemeVariation>} */
let ThemeVariations;
/**
* Set of prefs to observe.
*
* @type {!Array<string>}
*/
const PREFS = [
'theme',
'background-color',
'foreground-color',
'cursor-color',
'color-palette-overrides',
];
class Theme {
/**
* @param {string} id Theme ID.
* @param {string} translationKey Translation key.
* @param {string} background Background color.
* @param {string} font Font color.
* @param {string} cursor Cursor color.
* @param {!Array<string>} ansi ANSI 16 colors.
*/
constructor(id, translationKey, background, font, cursor, ansi) {
this.id = id;
this.translationKey = translationKey;
/**
* @private {!ThemeVariation}
* @const
*/
this.defaults_ = {
'theme': id,
'background-color': background,
'foreground-color': font,
'cursor-color': cursor,
'color-palette-overrides': ansi,
};
/** @private {!lib.PreferenceManager} */
this.preferenceManager_;
/** @private {!ThemeVariation} */
this.variations_ = {};
}
/**
* Initialize object with PreferenceManager and read variations.
*
* @param {!lib.PreferenceManager} preferenceManager
*/
init(preferenceManager) {
this.preferenceManager_ = preferenceManager;
this.variations_ = preferenceManager.get('theme-variations')[this.id] || {};
}
/**
* Get the theme's current value of the specified pref.
*
* @param {string} name Preference name.
* @return {*}
* @private
*/
getPref_(name) {
return this.variations_.hasOwnProperty(name)
? this.variations_[name] : this.defaults_[name];
}
/** @return {string} */
get background() {
return /** @type {string} */ (this.getPref_('background-color'));
}
/** @return {string} */
get font() {
return /** @type {string} */ (this.getPref_('foreground-color'));
}
/** @return {string} */
get cursor() { return /** @type {string} */ (this.getPref_('cursor-color')); }
/** @return {!Array<string>} */
get ansi() {
return /** @type {!Array<string>} */ (
this.getPref_('color-palette-overrides'));
}
/**
* Write current state to prefs.
*/
writeToPrefs() {
// Any non-priimitive stored into prefs must be duplicated to avoid storing
// the live copy which makes comparisons void.
PREFS.forEach((p) => {
let v = this.getPref_(p);
if (Array.isArray(v)) {
v = v.slice();
}
this.preferenceManager_.set(p, v);
});
}
/**
* Add a variation to this theme.
*
* @param {string} name
* @param {*} value
*/
setVariation(name, value) {
// Store local this.variations_ if it is different to default.
const d = this.defaults_[name];
if ((value === d) || (JSON.stringify(value) === JSON.stringify(d))) {
delete this.variations_[name];
} else {
this.variations_[name] = value;
}
// Update 'theme-variations' pref.
/** @type {!ThemeVariations} */
const tvs = /** @type {!ThemeVariations} */ (
this.preferenceManager_.get('theme-variations'));
const tv = tvs[this.id];
if (!this.hasVariations()) {
if (tv === undefined) {
return;
}
delete tvs[this.id];
} else {
const s = JSON.stringify(this.variations_);
if (s === JSON.stringify(tv)) {
return;
}
// Stringify and parse to duplicate before updating pref.
tvs[this.id] = /** @type {!ThemeVariation} */(JSON.parse(s));
}
this.preferenceManager_.set('theme-variations', tvs);
}
/** @return {boolean} Returns true if theme has a variation. */
hasVariations() {
return Object.keys(this.variations_).length > 0;
}
/** Reset theme and clear variations. */
clearVariations() {
this.variations_ = {};
/** @type {!ThemeVariations} */
const tvs = /** @type {!ThemeVariations} */ (
this.preferenceManager_.get('theme-variations'));
delete tvs[this.id];
this.preferenceManager_.set('theme-variations', tvs);
this.writeToPrefs();
}
}
/** @type {!Object<string, !Theme>} */
const THEMES = {
'dark': new Theme('dark',
'TERMINAL_THEME_DARK_LABEL', DEFAULT_BACKGROUND_COLOR,
DEFAULT_FOREGROUND_COLOR, DEFAULT_CURSOR_COLOR, DEFAULT_ANSI_COLORS),
'light': new Theme('light',
'TERMINAL_THEME_LIGHT_LABEL', '#FFFFFF', '#000000', '#1967D280',
['#E8EAED', '#F28B82', '#108468', '#F29900',
'#8AB4F8', '#F882FF', '#03BFC8', '#202124',
'#F8F9FA', '#EE675C', '#108468', '#DB7000',
'#1A73E8', '#AA00B8', '#009099', '#9AA0A6']),
'classic': new Theme('classic',
'TERMINAL_THEME_CLASSIC_LABEL', '#101010', '#FFFFFF', '#FF000080',
lib.colors.stockPalette.slice(0, 16)),
'solarizedDark': new Theme('solarizedDark',
'TERMINAL_THEME_SOLARIZED_DARK_LABEL', '#002B36', '#83949680',
'#93A1A180',
['#073642', '#DC322F', '#859900', '#B58900',
'#268BD2', '#D33682', '#2AA198', '#EEE8D5',
'#002B36', '#CB4B16', '#586E75', '#657B83',
'#839496', '#6C71C4', '#93A1A1', '#FDF6E3']),
'solarizedLight': new Theme('solarizedLight',
'TERMINAL_THEME_SOLARIZED_LIGHT_LABEL', '#FDF6E3', '#657B83', '#586E7580',
['#EEE8D5', '#DC322F', '#859900', '#B58900',
'#268BD2', '#D33682', '#2AA198', '#073642',
'#FDF6E3', '#CB4B16', '#93A1A1', '#839496',
'#657B83', '#6C71C4', '#586E75', '#002B36']),
'dusk': new Theme('dusk',
'TERMINAL_THEME_DUSK_LABEL', '#22273E', '#FFFFFF', '#87FFC580',
['#2D3452', '#F4B5FB', '#E3FEEF', '#E7F936',
'#9573F5', '#FFA08B', '#30E2EA', '#434D7B',
'#8D9CF6', '#F882FF', '#87FFC5', '#F1FF67',
'#B39AF5', '#FFBCAD', '#80F9F9', '#414976']),
'haze': new Theme('haze',
'TERMINAL_THEME_HAZE_LABEL', '#3E1C43', '#FFFFFF', '#FEEFC380',
['#5B3062', '#F6AEA9', '#CDD8FA', '#F7FFA4',
'#956FE4', '#F994FF', '#87FFC5', '#2C222E',
'#FBE1FF', '#F9C6C3', '#97B0FC', '#F1FF67',
'#D3BEFF', '#FBB4FF', '#ABFFD6', '#76427D']),
'forest': new Theme('forest',
'TERMINAL_THEME_FOREST_LABEL', '#1F3334', '#FFFFFF', '#CCB4FF80',
['#2B4E50', '#FFA07A', '#7097B0', '#F1FF67',
'#C6FFE3', '#FAA5FF', '#8584CD', '#202124',
'#AAFCFF', '#FFB79A', '#A2D3F2', '#DCF775',
'#87FFC5', '#FBB4FF', '#8B88FF', '#486B6C']),
};
/**
* Reset svg icon.
*
* @type {string}
*/
const RESET =
'<svg width="20px" height="20px" xmlns="http://www.w3.org/2000/svg">' +
'<path d="M3,5 A6,6 0 1,1 2,9 M2,2 v4 h4" ' +
'fill="none" stroke-width="1.75" stroke="white"/></svg>';
export class TerminalSettingsThemeElement extends LitElement {
static get is() { return 'terminal-settings-theme'; }
constructor() {
super();
/** @private {!Theme} */
this.theme_;
this.boundProfileChanged_ = this.profileChanged_.bind(this);
this.boundPreferenceChanged_ = this.preferenceChanged_.bind(this);
}
/** @override */
connectedCallback() {
super.connectedCallback();
this.profileChanged_();
window.preferenceManager.onPrefixChange.addListener(
this.boundProfileChanged_);
PREFS.forEach((p) => {
window.preferenceManager.addObserver(
p, this.boundPreferenceChanged_);
});
}
/** @override */
disconnectedCallback() {
super.disconnectedCallback();
window.preferenceManager.onPrefixChange.removeListener(
this.boundProfileChanged_);
PREFS.forEach((p) => {
window.preferenceManager.removeObserver(
p, this.boundPreferenceChanged_);
});
}
/** @override */
static get styles() {
return css`
#themes {
display: flex;
flex-wrap: wrap;
margin: 0;
max-width: 600px;
width: 100%;
}
.theme {
flex-basis: 25%;
margin: 0;
max-width: 150px;
}
.theme-inner {
border: 1px solid var(--cros-separator-color);
border-radius: 8px;
cursor: pointer;
height: 88px;
margin: 8px 0 0 8px;
outline: none;
overflow: hidden;
position: relative;
}
.theme-inner:focus-visible {
box-shadow: 0 0 0 2px var(--cros-color-prominent);
}
.theme:nth-child(-n+4) > .theme-inner {
margin: 0 0 0 8px;
}
.preview {
height: 48px;
min-width: 100%;
overflow: hidden;
position: relative;
}
.preview pre {
font-family: 'Noto Sans Mono';
font-size: 5px;
line-height: 6px;
margin: 0;
padding: 6px 8px;
position: absolute;
white-space: pre;
}
.label {
background-color: var(--cros-bg-color);
bottom: 0;
color: var(--cros-color-primary);
font-weight: 400;
line-height: 40px;
position: absolute;
width: 100%;
}
.label p {
margin: 0 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.theme[active-theme] .label {
background-color: var(--cros-highlight-color);
color: var(--cros-color-prominent);
}
.reset {
background-color: rgba(var(--cros-app-shield-color-rgb), 0.6);
bottom: 0;
color: #fff;
display: none;
font-size: 13px;
font-weight: 500;
left: 0;
margin: 0;
padding: 0;
position: absolute;
right: 0;
top: 0;
width: 100%;
z-index: 10;
}
.reset div {
background: url('data:image/svg+xml;utf8,${unsafeCSS(RESET)}')
no-repeat left;
line-height: 20px;
margin: 14px auto;
padding: 0 0 0 28px;
width: 38px;
}
.theme[active-theme][reset-theme] .reset {
display: block;
}
`;
}
/** @override */
render() {
const msg = hterm.messageManager.get.bind(hterm.messageManager);
const span = (color, text) => {
return html`<span style="color:${color};font-weight:bold">${text}</span>`;
};
return html`
<div id="themes">${Object.values(THEMES).map((t) => html`
<div id="${t.id}" class="theme"
?active-theme="${this.theme_.id === t.id}"
?reset-theme="${this.theme_.hasVariations()}">
<div class="theme-inner" tabindex="0"
aria-label="${
msg('TERMINAL_TITLE_THEME')} ${msg(t.translationKey)}"
@click="${this.onClicked_}"
@keydown="${this.onKeydown_}">
<div class="preview" aria-hidden="true"
style="background-color:${t.background};color:${t.font}">
<pre>drwxr-xr-x 1 joel 13:28 ${span(t.ansi[12], '.')}
drwxr-xr-x 1 root 07:00 ${span(t.ansi[12], '..')}
-rw-r--r-- 1 joel 15:24 .bashrc
drwxr-xr-x 1 joel 10:38 ${span(t.ansi[12], '.config')}
-rwxr-xr-x 1 joel 14:30 ${span(t.ansi[10], 'a.out')}
${span(t.ansi[10], 'joel@penguin')}:${span(t.ansi[12], '~')
}$ ls -al<span style="background:${t.cursor}"> </span></pre>
<div class="reset">
<div>${msg('TERMINAL_SETTINGS_RESET_LABEL')}</div>
</div>
</div>
<div class="label"><p>${msg(t.translationKey)}</p></div>
</div>
</div>`)}
</div>
<terminal-dialog acceptText="${msg('TERMINAL_SETTINGS_RESET_LABEL')}"
@close=${this.onDialogClose}>
<div slot="title">${msg('TERMINAL_SETTINGS_RESET_DIALOG_TITLE')}</div>
${msg('TERMINAL_SETTINGS_RESET_DIALOG_MESSAGE')}
</terminal-dialog>
`;
}
/**
* @param {string} id Theme clicked.
* @private
*/
onActivated_(id) {
if (!THEMES.hasOwnProperty(id)) {
console.error(`Unknown theme: ${id}`);
return;
}
if (this.theme_.id === id && this.theme_.hasVariations()) {
this.shadowRoot.querySelector('terminal-dialog').show();
} else {
this.theme_ = THEMES[id];
this.theme_.writeToPrefs();
}
}
/**
* @param {!Event} event
* @private
*/
onClicked_(event) {
this.onActivated_(event.currentTarget.parentNode.id);
event.preventDefault();
}
/**
* @param {!Event} event
* @private
*/
onKeydown_(event) {
switch (event.code) {
case 'Enter':
case 'Space':
this.onActivated_(event.currentTarget.parentNode.id);
event.preventDefault();
break;
}
}
/** @private */
profileChanged_() {
Object.values(THEMES).forEach((t) => {
t.init(window.preferenceManager);
});
this.preferenceChanged_(
window.preferenceManager.get('theme'), 'theme');
}
/**
* @param {*} value
* @param {string} name
* @protected
*/
preferenceChanged_(value, name) {
if (name === 'theme') {
if (!THEMES.hasOwnProperty(value)) {
value = DEFAULT_THEME;
}
this.theme_ = THEMES[/** @type {string} */(value)];
} else {
this.theme_.setVariation(name, value);
}
this.requestUpdate();
}
onDialogClose(event) {
if (event.detail.accept) {
this.theme_.clearVariations();
}
}
}
customElements.define(TerminalSettingsThemeElement.is,
TerminalSettingsThemeElement);