diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index b20ae8be..0c711c93 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -38,6 +38,7 @@

Guide

  • Blacklist
  • Search engines
  • Properties
  • +
  • Custom Styles
  • @@ -53,6 +54,7 @@

    Guide

  • Blacklist
  • Search engines
  • Properties
  • +
  • Custom Styles
  • GitHub
  • Firefox Add-ons
  • diff --git a/docs/styles.md b/docs/styles.md new file mode 100644 index 00000000..7ba4b901 --- /dev/null +++ b/docs/styles.md @@ -0,0 +1,31 @@ +--- +title: Custom Styles +--- + +# Custom Styles + +Users can specify custom styles in CSS format for certain components. + +```json +{ + "styles": { + "hint": { + "background-color": "yellow", + "border": "1px solid gold", + "font-weight": "bold", + "font-size": "12px", + "color": "black" + }, + "console": { + "font-family": "monospace", + "font-size": "12px" + } + } +} + +``` + +Vimmatic supports the two types of components: + +- `hint`: Hint tags in follow mode. +- `console`: Console at the bottom of the page. diff --git a/script/build b/script/build index 39e317a4..e5e9c0e2 100755 --- a/script/build +++ b/script/build @@ -30,7 +30,6 @@ const buildScripts = async (browser) => { const buildAssets = async (browser) => { await fs.cp("resources/", `dist/${browser}/resources/`, { recursive: true }); - await fs.copyFile(`src/console/index.css`, `dist/${browser}/lib/console.css`); await fs.copyFile( `src/console/index.html`, `dist/${browser}/lib/console.html` diff --git a/src/background/controllers/SettingsController.ts b/src/background/controllers/SettingsController.ts index 570e3d8e..354e8f84 100644 --- a/src/background/controllers/SettingsController.ts +++ b/src/background/controllers/SettingsController.ts @@ -24,6 +24,17 @@ export default class SettingsController { return this.settingsUseCase.getProperty(name); } + async getStyle( + _ctx: RequestContext, + { + name, + }: { + name: string; + } + ): Promise> { + return this.settingsUseCase.getStyle(name); + } + async validate( _ctx: RequestContext, { settings }: { settings: unknown } diff --git a/src/background/di.ts b/src/background/di.ts index 23215513..22a93a17 100644 --- a/src/background/di.ts +++ b/src/background/di.ts @@ -21,6 +21,7 @@ import { ChromeClipboardRepositoryImpl, } from "./repositories/ClipboardRepository"; import { PropertySettingsImpl } from "./settings/PropertySettings"; +import { StyleSettingsImpl } from "./settings/StyleSettings"; import { SearchEngineSettingsImpl } from "./settings/SearchEngineSettings"; import { TransientSettingsRepository, @@ -65,6 +66,7 @@ if (process.env.BROWSER === "firefox") { .to(ChromeBrowserSettingRepositoryImpl); } container.bind("PropertySettings").to(PropertySettingsImpl); +container.bind("StyleSettings").to(StyleSettingsImpl); container.bind("SearchEngineSettings").to(SearchEngineSettingsImpl); container.bind("KeyCaptureClient").to(KeyCaptureClientImpl); container.bind("MarkRepository").to(MarkRepositoryImpl); diff --git a/src/background/messaging/BackgroundMessageListener.ts b/src/background/messaging/BackgroundMessageListener.ts index 147754d9..d858f43d 100644 --- a/src/background/messaging/BackgroundMessageListener.ts +++ b/src/background/messaging/BackgroundMessageListener.ts @@ -60,6 +60,9 @@ export default class BackgroundMessageListener { this.receiver .route("settings.get.property") .to(settingsController.getProperty.bind(settingsController)); + this.receiver + .route("settings.get.style") + .to(settingsController.getStyle.bind(settingsController)); this.receiver .route("settings.query") .to(settingsController.getSettings.bind(settingsController)); diff --git a/src/background/settings/StyleSettings.ts b/src/background/settings/StyleSettings.ts new file mode 100644 index 00000000..bc598928 --- /dev/null +++ b/src/background/settings/StyleSettings.ts @@ -0,0 +1,25 @@ +import { injectable, inject } from "inversify"; +import SettingsRepository from "./SettingsRepository"; +import { ComponentName } from "../../shared/Styles"; +import { defaultSettings } from "../../settings"; + +export default interface StyleSettings { + getStyle(name: string): Promise>; +} + +@injectable() +export class StyleSettingsImpl { + constructor( + @inject("SettingsRepository") + private readonly settingsRepository: SettingsRepository + ) {} + + async getStyle(name: ComponentName): Promise> { + const settings = await this.settingsRepository.load(); + const value = (settings.styles || {})[name]; + if (typeof value === "undefined") { + return defaultSettings.styles![name]!; + } + return value; + } +} diff --git a/src/background/usecases/SettingsUseCase.ts b/src/background/usecases/SettingsUseCase.ts index 8a794a35..5ab807e2 100644 --- a/src/background/usecases/SettingsUseCase.ts +++ b/src/background/usecases/SettingsUseCase.ts @@ -2,6 +2,7 @@ import { injectable, inject } from "inversify"; import { serialize, deserialize } from "../../settings"; import SettingsRepository from "../settings/SettingsRepository"; import PropertySettings from "../settings/PropertySettings"; +import StyleSettings from "../settings/StyleSettings"; import Validator from "../settings/Validator"; @injectable() @@ -11,6 +12,8 @@ export default class SettingsUseCase { private readonly settingsRepository: SettingsRepository, @inject("PropertySettings") private readonly propertySettings: PropertySettings, + @inject("StyleSettings") + private readonly styleSettings: StyleSettings, @inject(Validator) private readonly validator: Validator ) {} @@ -23,6 +26,10 @@ export default class SettingsUseCase { return this.propertySettings.getProperty(name); } + async getStyle(name: string): Promise> { + return this.styleSettings.getStyle(name); + } + async validate(data: unknown): Promise { try { this.validator.validate(deserialize(data)); diff --git a/src/console/App.tsx b/src/console/App.tsx index f881fb38..2517a971 100644 --- a/src/console/App.tsx +++ b/src/console/App.tsx @@ -1,19 +1,51 @@ -import Console from "./components/Console"; +import React from "react"; +import Prompt from "./components/Prompt"; +import InfoMessage from "./components/InfoMessage"; +import ErrorMessage from "./components/ErrorMessage"; import { SimplexReceiver } from "../messaging"; import type { Schema as ConsoleMessageSchema } from "../messaging/schema/console"; -import React from "react"; -import { useConsoleMode, useVisibility } from "./app/hooks"; -import { useColorSchemeRefresh } from "./colorscheme/hooks"; +import { + useConsoleMode, + useVisibility, + useExecCommand, + useGetCommandCompletion, + useExecFind, + useGetFindCompletion, +} from "./app/hooks"; +import { useInvalidateStyle } from "./styles/hooks"; + +// (10item + 1title) * sbc +const COMMAND_COMPLETION_MAX_ITEMS = 33; +const FIND_COMPLETION_MAX_ITEMS = 11; const App: React.FC = () => { - const refreshColorScheme = useColorSchemeRefresh(); + const refreshColorScheme = useInvalidateStyle(); const { hide, visible } = useVisibility(); const { + state, showCommandPrompt, showFindPrompt, showInfoMessage, showErrorMessage, } = useConsoleMode(); + const execCommand = useExecCommand(); + const execFind = useExecFind(); + const getCommandCompletions = useGetCommandCompletion(); + const getFindCompletions = useGetFindCompletion(); + const onExec = React.useCallback( + (cmd: string) => { + if (state.mode !== "prompt") { + return; + } + if (state.promptMode === "command") { + execCommand(cmd); + } else if (state.promptMode === "find") { + execFind(cmd); + } + hide(); + }, + [execCommand, execFind, state] + ); React.useEffect(() => { if (visible) { @@ -41,7 +73,36 @@ const App: React.FC = () => { }); }, []); - return ; + if (state.mode === "prompt") { + if (state.promptMode === "command") { + return ( + + ); + } else if (state.promptMode === "find") { + return ( + + ); + } + } else if (state.mode === "info_message") { + return {state.message}; + } else if (state.mode === "error_message") { + return {state.message}; + } + return null; }; export default App; diff --git a/src/console/clients/SettingClient.ts b/src/console/clients/SettingClient.ts index 13f8a07c..50fdf87c 100644 --- a/src/console/clients/SettingClient.ts +++ b/src/console/clients/SettingClient.ts @@ -9,4 +9,11 @@ export default class SettingClient { }); return value as string; } + + async getConsoleStyle(): Promise> { + const value = await this.sender.send("settings.get.style", { + name: "console", + }); + return value as Record; + } } diff --git a/src/console/colorscheme/contexts.tsx b/src/console/colorscheme/contexts.tsx deleted file mode 100644 index dba4eb67..00000000 --- a/src/console/colorscheme/contexts.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; - -export const ColorSchemeContext = React.createContext("system"); - -export const ColorSchemeUpdateContext = React.createContext< - (colorscheme: string) => void ->(() => {}); diff --git a/src/console/colorscheme/hooks.ts b/src/console/colorscheme/hooks.ts deleted file mode 100644 index 6a840475..00000000 --- a/src/console/colorscheme/hooks.ts +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; -import { ColorSchemeUpdateContext } from "./contexts"; -import SettingClient from "../clients/SettingClient"; -import { newSender } from "../clients/BackgroundMessageSender"; - -const settingClient = new SettingClient(newSender()); - -export const useColorSchemeRefresh = () => { - const update = React.useContext(ColorSchemeUpdateContext); - const refresh = React.useCallback(() => { - settingClient.getColorScheme().then((newScheme) => { - update(newScheme); - }); - }, []); - - return refresh; -}; diff --git a/src/console/colorscheme/providers.tsx b/src/console/colorscheme/providers.tsx deleted file mode 100644 index 9153f2bd..00000000 --- a/src/console/colorscheme/providers.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; -import { DarkTheme, LightTheme } from "./theme"; -import { ColorSchemeContext, ColorSchemeUpdateContext } from "./contexts"; -import { ThemeProvider } from "styled-components"; - -export const ColorSchemeProvider: React.FC = ({ children }) => { - const [colorscheme, setColorScheme] = React.useState("system"); - const theme = React.useMemo(() => { - if (colorscheme === "system") { - if ( - window.matchMedia && - window.matchMedia("(prefers-color-scheme: dark)").matches - ) { - return DarkTheme; - } - } else if (colorscheme === "dark") { - return DarkTheme; - } - return LightTheme; - }, [colorscheme]); - - return ( - - - {children} - - - ); -}; -export default ColorSchemeProvider; diff --git a/src/console/colorscheme/theme.ts b/src/console/colorscheme/theme.ts deleted file mode 100644 index 5c171904..00000000 --- a/src/console/colorscheme/theme.ts +++ /dev/null @@ -1,47 +0,0 @@ -export type ThemeProperties = { - completionTitleBackground: string; - completionTitleForeground: string; - completionItemBackground: string; - completionItemForeground: string; - completionItemDescriptionForeground: string; - completionSelectedBackground: string; - completionSelectedForeground: string; - commandBackground: string; - commandForeground: string; - consoleErrorBackground: string; - consoleErrorForeground: string; - consoleInfoBackground: string; - consoleInfoForeground: string; -}; - -export const LightTheme: ThemeProperties = { - completionTitleBackground: "lightgray", - completionTitleForeground: "#000000", - completionItemBackground: "#ffffff", - completionItemForeground: "#000000", - completionItemDescriptionForeground: "#008000", - completionSelectedBackground: "#ffff00", - completionSelectedForeground: "#000000", - commandBackground: "#ffffff", - commandForeground: "#000000", - consoleErrorBackground: "#ff0000", - consoleErrorForeground: "#ffffff", - consoleInfoBackground: "#ffffff", - consoleInfoForeground: "#018786", -}; - -export const DarkTheme: ThemeProperties = { - completionTitleBackground: "#052027", - completionTitleForeground: "white", - completionItemBackground: "#2f474f", - completionItemForeground: "white", - completionItemDescriptionForeground: "#86fab0", - completionSelectedBackground: "#eeff41", - completionSelectedForeground: "#000000", - commandBackground: "#052027", - commandForeground: "white", - consoleErrorBackground: "red", - consoleErrorForeground: "white", - consoleInfoBackground: "#052027", - consoleInfoForeground: "#ffffff", -}; diff --git a/src/console/completion/components/CompletionItem.tsx b/src/console/completion/components/CompletionItem.tsx index 415fecb5..96513f21 100644 --- a/src/console/completion/components/CompletionItem.tsx +++ b/src/console/completion/components/CompletionItem.tsx @@ -1,5 +1,5 @@ import React from "react"; -import styled from "../../colorscheme/styled"; +import styled from "../../styles/styled"; const Container = styled.li<{ shown: boolean; @@ -9,13 +9,9 @@ const Container = styled.li<{ background-image: ${({ icon }) => typeof icon !== "undefined" ? "url(" + icon + ")" : "unset"}; background-color: ${({ highlight, theme }) => - highlight - ? theme.completionSelectedBackground - : theme.completionItemBackground}; + highlight ? theme.select?.background : theme.background}; color: ${({ highlight, theme }) => - highlight - ? theme.completionSelectedForeground - : theme.completionItemForeground}; + highlight ? theme.select?.foreground : theme.foreground}; display: ${({ shown }) => (shown ? "block" : "none")}; padding-left: 1.8rem; background-position: 0 center; @@ -33,7 +29,7 @@ const Primary = styled.span` const Secondary = styled.span` display: inline-block; - color: ${({ theme }) => theme.completionItemDescriptionForeground}; + color: ${({ theme }) => theme.secondaryForeground}; width: 60%; text-overflow: ellipsis; overflow: hidden; diff --git a/src/console/completion/components/CompletionTitle.tsx b/src/console/completion/components/CompletionTitle.tsx index 99c3eef0..196e950a 100644 --- a/src/console/completion/components/CompletionTitle.tsx +++ b/src/console/completion/components/CompletionTitle.tsx @@ -1,10 +1,10 @@ import React from "react"; -import styled from "../../colorscheme/styled"; +import styled from "../../styles/styled"; const Li = styled.li<{ shown: boolean }>` display: ${({ shown }) => (shown ? "display" : "none")}; - background-color: ${({ theme }) => theme.completionTitleBackground}; - color: ${({ theme }) => theme.completionTitleForeground}; + background-color: ${({ theme }) => theme.title?.background}; + color: ${({ theme }) => theme.title?.foreground}; list-style: none; font-weight: bold; margin: 0; diff --git a/src/console/components/Console.tsx b/src/console/components/Console.tsx deleted file mode 100644 index ad024cfd..00000000 --- a/src/console/components/Console.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from "react"; -import Prompt from "./Prompt"; -import InfoMessage from "./InfoMessage"; -import ErrorMessage from "./ErrorMessage"; -import { useConsoleMode } from "../app/hooks"; -import { - useExecCommand, - useGetCommandCompletion, - useExecFind, - useGetFindCompletion, - useVisibility, -} from "../app/hooks"; - -// (10item + 1title) * sbc -const COMMAND_COMPLETION_MAX_ITEMS = 33; -const FIND_COMPLETION_MAX_ITEMS = 11; - -const Console: React.FC = () => { - const { hide } = useVisibility(); - const { state } = useConsoleMode(); - const execCommand = useExecCommand(); - const execFind = useExecFind(); - const getCommandCompletions = useGetCommandCompletion(); - const getFindCompletions = useGetFindCompletion(); - - const onExec = React.useCallback( - (cmd: string) => { - if (state.mode !== "prompt") { - return; - } - if (state.promptMode === "command") { - execCommand(cmd); - } else if (state.promptMode === "find") { - execFind(cmd); - } - hide(); - }, - [execCommand, execFind, state] - ); - - if (state.mode === "prompt") { - if (state.promptMode === "command") { - return ( - - ); - } else if (state.promptMode === "find") { - return ( - - ); - } - } else if (state.mode === "info_message") { - return {state.message}; - } else if (state.mode === "error_message") { - return {state.message}; - } - return null; -}; - -export default Console; diff --git a/src/console/components/ErrorMessage.tsx b/src/console/components/ErrorMessage.tsx index 4830a555..3b8435ac 100644 --- a/src/console/components/ErrorMessage.tsx +++ b/src/console/components/ErrorMessage.tsx @@ -1,11 +1,11 @@ import React from "react"; -import styled from "../colorscheme/styled"; +import styled from "../styles/styled"; import useAutoResize from "../hooks/useAutoResize"; const Wrapper = styled.p` border-top: 1px solid gray; - background-color: ${({ theme }) => theme.consoleErrorBackground}; - color: ${({ theme }) => theme.consoleErrorForeground}; + background-color: ${({ theme }) => theme.error?.background}; + color: ${({ theme }) => theme.error?.foreground}; font-weight: bold; `; diff --git a/src/console/components/InfoMessage.tsx b/src/console/components/InfoMessage.tsx index 80fa8755..e72b99a3 100644 --- a/src/console/components/InfoMessage.tsx +++ b/src/console/components/InfoMessage.tsx @@ -1,11 +1,11 @@ import React from "react"; -import styled from "../colorscheme/styled"; +import styled from "../styles/styled"; import useAutoResize from "../hooks/useAutoResize"; const Wrapper = styled.p` border-top: 1px solid gray; - background-color: ${({ theme }) => theme.consoleInfoBackground}; - color: ${({ theme }) => theme.consoleInfoForeground}; + background-color: ${({ theme }) => theme.info?.background}; + color: ${({ theme }) => theme.info?.foreground}; font-weight: normal; `; diff --git a/src/console/components/PromptInput.tsx b/src/console/components/PromptInput.tsx index 161b052c..dd4d2444 100644 --- a/src/console/components/PromptInput.tsx +++ b/src/console/components/PromptInput.tsx @@ -1,9 +1,9 @@ import React, { InputHTMLAttributes } from "react"; -import styled from "../colorscheme/styled"; +import styled from "../styles/styled"; const Container = styled.div` - background-color: ${({ theme }) => theme.commandBackground}; - color: ${({ theme }) => theme.commandForeground}; + background-color: ${({ theme }) => theme.command.background}; + color: ${({ theme }) => theme.command.foreground}; display: flex; `; @@ -14,8 +14,8 @@ const Prompt = styled.i` const InputInner = styled.input` border: none; flex-grow: 1; - background-color: ${({ theme }) => theme.commandBackground}; - color: ${({ theme }) => theme.commandForeground}; + background-color: ${({ theme }) => theme.command.background}; + color: ${({ theme }) => theme.command.foreground}; `; interface Props extends InputHTMLAttributes { diff --git a/src/console/index.css b/src/console/index.css deleted file mode 100644 index 95b180e3..00000000 --- a/src/console/index.css +++ /dev/null @@ -1,30 +0,0 @@ -html, body, * { - margin: 0; - padding: 0; - - font-style: normal; - font-family: monospace; - font-size: 12px; - line-height: 16px; -} - -input { - font-style: normal; - font-family: monospace; - font-size: 12px; - line-height: 16px; -} - -body { - position: absolute; - bottom: 0; - left: 0; - right: 0; - overflow: hidden; -} - -.vimmatic-console { - bottom: 0; - margin: 0; - padding: 0; -} diff --git a/src/console/index.html b/src/console/index.html index 47de482b..70503997 100644 --- a/src/console/index.html +++ b/src/console/index.html @@ -4,9 +4,8 @@ Vimmatic console - -
    +
    diff --git a/src/console/index.tsx b/src/console/index.tsx index 5464037b..45eb7f2a 100644 --- a/src/console/index.tsx +++ b/src/console/index.tsx @@ -1,6 +1,6 @@ import React from "react"; import ReactDOM from "react-dom"; -import ColorSchemeProvider from "./colorscheme/providers"; +import ColorSchemeProvider from "./styles/providers"; import { AppProvider } from "./app/provider"; import App from "./App"; diff --git a/src/console/styles/contexts.tsx b/src/console/styles/contexts.tsx new file mode 100644 index 00000000..4d6c2c0c --- /dev/null +++ b/src/console/styles/contexts.tsx @@ -0,0 +1,10 @@ +import React from "react"; + +export type Style = { + colorscheme: string; + css: Record; +}; + +export const UpdateStyleContext = React.createContext<(style: Style) => void>( + () => {} +); diff --git a/src/console/styles/global.ts b/src/console/styles/global.ts new file mode 100644 index 00000000..b01ea66c --- /dev/null +++ b/src/console/styles/global.ts @@ -0,0 +1,23 @@ +import { createGlobalStyle, css } from "styled-components"; +import type { CSSObject } from "styled-components"; + +const GlobalStyle = createGlobalStyle` + html, body, * { + margin: 0; + padding: 0; + } + + body { + position: absolute; + bottom: 0; + left: 0; + right: 0; + overflow: hidden; + } + + * { + ${(obj) => css(obj)} + } +`; + +export default GlobalStyle; diff --git a/src/console/styles/hooks.ts b/src/console/styles/hooks.ts new file mode 100644 index 00000000..fe688965 --- /dev/null +++ b/src/console/styles/hooks.ts @@ -0,0 +1,17 @@ +import React from "react"; +import { UpdateStyleContext } from "./contexts"; +import SettingClient from "../clients/SettingClient"; +import { newSender } from "../clients/BackgroundMessageSender"; + +const settingClient = new SettingClient(newSender()); + +export const useInvalidateStyle = () => { + const update = React.useContext(UpdateStyleContext); + const refresh = React.useCallback(async () => { + const colorscheme = await settingClient.getColorScheme(); + const css = await settingClient.getConsoleStyle(); + update({ colorscheme, css }); + }, []); + + return refresh; +}; diff --git a/src/console/styles/providers.tsx b/src/console/styles/providers.tsx new file mode 100644 index 00000000..9dbe8764 --- /dev/null +++ b/src/console/styles/providers.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { DarkTheme, LightTheme } from "./theme"; +import { Style, UpdateStyleContext } from "./contexts"; +import GlobalStyle from "./global"; +import { ThemeProvider } from "styled-components"; + +export const StyleProvider: React.FC = ({ children }) => { + const [style, setStyle] = React.useState