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
-
-
+