Skip to content

Commit

Permalink
Merge pull request #1415 from klembot/add-user-css
Browse files Browse the repository at this point in the history
Load user.css in Electron
  • Loading branch information
klembot authored Jul 7, 2023
2 parents dbb0e3f + dbec1ce commit 5270507
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 7 deletions.
2 changes: 2 additions & 0 deletions .github/ISSUE_TEMPLATE/bug-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,7 @@ body:
required: false
- label: I have done a search and believe that an issue does not already exist for this bug in the GitHub repository.
required: true
- label: If this problem is occurring with the app version of Twine, it still happens after I remove the `user.css` file from my Twine folder. (You would've added this file yourself. If you don't understand this question, you can safely check this box.)
required: true
- label: I have read and agree to abide by this project's Code of Conduct.
required: true
4 changes: 3 additions & 1 deletion docs/en/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@
- [Removing a Story Format](story-formats/removing.md)
- [Disabling Story Format Extensions](story-formats/extensions.md)
- [How Twine Manages Story Format Versions](story-formats/versions.md)
- [Customing Twine With Preferences](preferences/index.md)
- [Customizing Twine](customizing/index.md)
- [Setting Preferences](customizing/preferences.md)
- [Advanced Customization](customizing/advanced.md)
- [Limitations](limitations/index.md)
- [Large Stories](limitations/large-stories.md)
- [Combining Stories](limitations/combining.md)
Expand Down
56 changes: 56 additions & 0 deletions docs/en/src/customizing/advanced.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Advanced Customization

## CSS

App Twine allows advanced customization its interface by creating a special
file, `user.css`, in your Twine folder. You'll need to create this file outside
of Twine using a text editor.

If `user.css` exists, then app Twine will add the contents of it to the UI as
CSS rules, potentially overriding the default styling. CSS allows changing the
appearance of the application--using different fonts, for example, or
colors--but does not allow changing Twine's functionality or adding new
features.

Here's a sample `user.css` that replaces the graph paper background of the story
map with a plain gray color:

```
.passage-map {
background: hsl(0, 0%, 75%) !important;
}
[data-app-theme="dark"] .passage-map {
background: hsl(0, 0%, 30%) !important;
}
```

`user.css` is only available in app Twine. If you'd like to customize browser
Twine using CSS, browser extensions like
[Stylus](https://github.com/openstyles/stylus/wiki) might help.

Some important things to keep in mind working with `user.css`:

- **The structure of Twine's UI can and will change on every release, even for
patch-level version changes.** Because these changes are often numerous, they
will not be part of release notes.
- The file must named exactly `user.css`--all lowercase. `User.css` will not
work.
- Changes to `user.css` will take effect the next time you start Twine.
- To determine what [CSS
selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) to
use, you can either use developer tools in browser Twine--the DOM structure is
identical between browser Twine and app Twine--or open developer tools in
Twine itself by going to _Troubleshooting_ under the _Help_ menu, then
choosing _Show Debug Console_.
- You can use the in-app debug console to test your CSS rules. The rules you set
in `user.css` will be listed as _injected stylesheet_ in the developer
console.
- `user.css` must be at the same folder level as your `Stories` and `Backups`
folders, directly below the `Twine` folder.
- If there's a problem loading `user.css`, Twine will load as normal and not
apply any customizations. If any of your CSS rules are incorrectly written,
they will be ignored. Twine will not show a warning in any of these cases.
- In order for `user.css` to [win
specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity),
you might need to add `!important` to the end of your declarations.
3 changes: 3 additions & 0 deletions docs/en/src/customizing/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Customizing Twine

This section describes how you can customize Twine to fit your needs.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Customing Twine With Preferences
# Setting Preferences

To customize Twine, choose _Preferences_ from the _Twine_ top toolbar tab. This
tab is available throughout Twine. A dialog will appear that lets you change
settings. These changes will take effect as soon as you make them, and Twine
will remember them between sessions.
To customize Twine's preferences, choose _Preferences_ from the _Twine_ top
toolbar tab. This tab is available throughout Twine. A dialog will appear that
lets you change settings. These changes will take effect as soon as you make
them, and Twine will remember them between sessions.

## Changing Twine's Language

Expand Down
3 changes: 3 additions & 0 deletions public/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@
"error": "Something went wrong while checking for an updated version of Twine.",
"updateAvailable": "A newer version of Twine is available.",
"upToDate": "This is the newest version of Twine available."
},
"userCss": {
"filename": "user.css"
}
},
"routes": {
Expand Down
1 change: 1 addition & 0 deletions src/electron/main-process/__tests__/init-app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ describe('initApp', () => {
});

it.todo('creates the main window');
it.todo('injects user CSS into the main window if available');

it('does not show an error dialog when everything loads', async () => {
await initApp();
Expand Down
31 changes: 31 additions & 0 deletions src/electron/main-process/__tests__/user-css.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {readFile} from 'fs-extra';
import {getUserCss} from '../user-css';

jest.mock('fs-extra');

describe('getUserCss', () => {
const readFileMock = readFile as jest.Mock;

it("returns the contents of user.css in the user's Twine directory", async () => {
readFileMock.mockReturnValue('mock-css');

expect(await getUserCss()).toBe('mock-css');
expect(readFileMock.mock.calls).toEqual([
[
'mock-electron-app-path-documents/common.appName/electron.userCss.filename',
'utf8'
]
]);
});

it('returns undefined and if the file could not be read', async () => {
const warnSpy = jest
.spyOn(global.console, 'warn')
.mockImplementation(() => {});

readFileMock.mockImplementation(() => Promise.reject(new Error()));

expect(await getUserCss()).toBeUndefined();
expect(warnSpy).toBeCalledTimes(1);
});
});
10 changes: 9 additions & 1 deletion src/electron/main-process/init-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {initIpc} from './ipc';
import {initLocales} from './locales';
import {initMenuBar} from './menu-bar';
import {backupStoryDirectory, createStoryDirectory} from './story-directory';
import {getUserCss} from './user-css';

let mainWindow: BrowserWindow | null;

Expand All @@ -28,7 +29,14 @@ async function createWindow() {
`file://${path.resolve(__dirname, '../../../../renderer/index.html')}`
);

mainWindow.once('ready-to-show', () => {
mainWindow.once('ready-to-show', async () => {
const userCss = await getUserCss();

if (userCss) {
console.log('Adding user CSS');
mainWindow!.webContents.insertCSS(userCss);
}

mainWindow!.show();

if (!app.isPackaged) {
Expand Down
21 changes: 21 additions & 0 deletions src/electron/main-process/user-css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {app} from 'electron';
import {readFile} from 'fs-extra';
import {join} from 'path';
import {i18n} from './locales';

export async function getUserCss(): Promise<string | undefined> {
try {
return await readFile(
join(
app.getPath('documents'),
i18n.t('common.appName'),
i18n.t('electron.userCss.filename')
),
'utf8'
);
} catch (error) {
console.warn(
`Error while loading user CSS, skipping: ${(error as Error).message}`
);
}
}

0 comments on commit 5270507

Please sign in to comment.