Skip to content

Commit 60d7bd3

Browse files
authored
Add Lucide icon theme (#2632)
2 parents 391f9bc + 266618d commit 60d7bd3

File tree

7 files changed

+211
-15
lines changed

7 files changed

+211
-15
lines changed

CREDITS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ constraints. These themes include a subset of icons from:
6969

7070
* **Font Awesome** by Fonticons Inc (CC BY 4.0)
7171
* **Remix** by RemixIcon (Apache 2.0 w/non-free clause)
72+
* **Lucide** by Cole Bemis and Lucide Contributors (ISC License)
7273

7374
## Colour Themes
7475

novelwriter/assets/text/credits_en.htm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ <h3>Icon Themes</h3>
8686
<ul>
8787
<li><b>Font Awesome</b> by Fonticons Inc (CC BY 4.0)</li>
8888
<li><b>Remix</b> by RemixIcon (Apache 2.0 w/non-free clause)</li>
89+
<li><b>Lucide</b> by Cole Bemis and Lucide Contributors (ISC License)</li>
8990
</ul>
9091

9192
<h3>Colour Themes</h3>

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ name = "novelWriter"
77
authors = [{ name = "Veronica Berglyd Olsen", email = "code@vkbo.net" }]
88
description = "A plain text editor for planning and writing novels"
99
readme = { file = "setup/description_pypi.md", content-type = "text/markdown" }
10-
license = "GPL-3.0-or-later AND Apache-2.0 AND CC-BY-4.0"
10+
license = "GPL-3.0-or-later AND Apache-2.0 AND CC-BY-4.0 AND ISC"
1111
license-files = ["LICENSE.md", "setup/LICENSE-Apache-2.0.txt"]
1212
dynamic = ["version"]
1313
requires-python = ">=3.11"

setup/debian/copyright

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,23 @@ License: CC-BY-4.0
5656
International. To view a copy of this license, visit
5757
.
5858
https://creativecommons.org/licenses/by/4.0/
59+
60+
Files: novelwriter/assets/icons/lucide.*
61+
Copyright: Cole Bemis, Lucide Contributors
62+
License: ISC
63+
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2023
64+
as part of Feather (MIT). All other copyright (c) for Lucide are held
65+
by Lucide Contributors 2025.
66+
.
67+
Permission to use, copy, modify, and/or distribute this software for
68+
any purpose with or without fee is hereby granted, provided that the
69+
above copyright notice and this permission notice appear in all copies.
70+
.
71+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
72+
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
73+
WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR
74+
BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
75+
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
76+
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
77+
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
78+
SOFTWARE.

utils/icon_themes.py

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@
3737
ICON_SOURCES = {
3838
"material": "https://github.com/google/material-design-icons.git",
3939
"font_awesome": "https://github.com/FortAwesome/Font-Awesome/archive/refs/tags/7.1.0.zip",
40-
"remix": "https://github.com/Remix-Design/RemixIcon/archive/refs/tags/v4.6.0.zip",
40+
"remix": "https://github.com/Remix-Design/RemixIcon/archive/refs/tags/v4.8.0.zip",
41+
"lucide": "https://github.com/lucide-icons/lucide/archive/refs/tags/0.562.0.zip",
4142
}
4243
ICON_EXTRACT = {
4344
"material": "material-design-icons",
4445
"font_awesome": "Font-Awesome-7.1.0",
45-
"remix": "RemixIcon-4.6.0",
46+
"remix": "RemixIcon-4.8.0",
47+
"lucide": "lucide-0.562.0",
4648
}
4749
ICONS = [
4850
# Remember to also update tests/files/all_icons.json for test coverage
@@ -189,14 +191,6 @@ def _loadMap(name: str) -> dict[str, str]:
189191
return icons
190192

191193

192-
def _fixXml(svg: ET.Element) -> str:
193-
"""Clean up the SVG XML and add needed fields."""
194-
svg.set("fill", "#000000")
195-
svg.set("height", "128")
196-
svg.set("width", "128")
197-
return ET.tostring(svg).decode()
198-
199-
200194
def _writeThemeFile(
201195
path: Path, name: str, author: str, license_: str, icons: dict[str, ET.Element]
202196
) -> None:
@@ -210,7 +204,8 @@ def _writeThemeFile(
210204
out.write("\n")
211205
out.write("# Icons\n")
212206
for key, svg in icons.items():
213-
out.write(f"icon:{key:<15s} = {_fixXml(svg)}\n")
207+
icon = ET.tostring(svg).decode().replace("\n", "")
208+
out.write(f"icon:{key:<15s} = {icon}\n")
214209
print(f"- Wrote: {len(icons)} icons")
215210
print(f"- Target: {path.relative_to(UTILS.parent)}")
216211

@@ -261,7 +256,11 @@ def processMaterialIcons(workDir: Path, iconsDir: Path, jobs: dict) -> None:
261256
fileName = f"{icon}_24px.svg"
262257
iconFile = iconSrc / icon / f"materialsymbols{style}" / fileName
263258
if iconFile.is_file():
264-
icons[key] = ET.fromstring(iconFile.read_text(encoding="utf-8"))
259+
svg = ET.fromstring(iconFile.read_text(encoding="utf-8"))
260+
svg.set("fill", "#000000")
261+
svg.set("height", "128")
262+
svg.set("width", "128")
263+
icons[key] = svg
265264
else:
266265
print(f"Not Found: {iconFile}")
267266

@@ -304,6 +303,9 @@ def processFontAwesome(workDir: Path, iconsDir: Path, jobs: dict) -> None:
304303
viewbox = [int(x) for x in svg.get("viewBox", "").split()]
305304
viewbox = [viewbox[2]//2 - 256, 0, 512, 512]
306305
svg.set("viewBox", " ".join(str(x) for x in viewbox))
306+
svg.set("fill", "#000000")
307+
svg.set("height", "128")
308+
svg.set("width", "128")
307309
for elem in svg.iter():
308310
elem.attrib.pop("fill", None)
309311
icons[key] = svg
@@ -317,6 +319,37 @@ def processFontAwesome(workDir: Path, iconsDir: Path, jobs: dict) -> None:
317319
print("")
318320

319321

322+
def processLucide(workDir: Path, iconsDir: Path, jobs: dict) -> None:
323+
"""Process Lucide icons of a given spec and write output file."""
324+
srcRepo = workDir / ICON_EXTRACT["lucide"]
325+
if not srcRepo.is_dir():
326+
_downloadIconPack(workDir, "lucide")
327+
328+
for file, job in jobs.items():
329+
name: str = job["name"]
330+
print(f"Processing: {name}")
331+
332+
icons: dict[str, ET.Element] = {}
333+
iconSrc = srcRepo / "icons"
334+
for key, icon in _loadMap("lucide").items():
335+
iconFile = iconSrc / f"{icon}.svg"
336+
if iconFile.is_file():
337+
svg = ET.fromstring(iconFile.read_text(encoding="utf-8"))
338+
ET.indent(svg, space="")
339+
svg.set("fill", "none")
340+
svg.set("stroke", "#000000")
341+
svg.set("height", "128")
342+
svg.set("width", "128")
343+
icons[key] = svg
344+
else:
345+
print(f"Not Found: {iconFile}")
346+
347+
target = iconsDir / f"{file}.icons"
348+
_writeThemeFile(target, name, "Cole Bemis, Lucide Contributors", "ISC/MIT License", icons)
349+
350+
print("")
351+
352+
320353
def processRemix(workDir: Path, iconsDir: Path, jobs: dict) -> None:
321354
"""Process Remix icons of a given spec and write output file."""
322355
srcRepo = workDir / ICON_EXTRACT["remix"]
@@ -351,7 +384,11 @@ def processRemix(workDir: Path, iconsDir: Path, jobs: dict) -> None:
351384
print(f"Not Found: {fileName}")
352385
continue
353386

354-
icons[key] = ET.fromstring(iconFile.read_text(encoding="utf-8"))
387+
svg = ET.fromstring(iconFile.read_text(encoding="utf-8"))
388+
svg.set("fill", "#000000")
389+
svg.set("height", "128")
390+
svg.set("width", "128")
391+
icons[key] = svg
355392

356393
target = iconsDir / f"{file}.icons"
357394
_writeThemeFile(target, name, "Remix Icon", "Apache 2.0", icons)
@@ -418,6 +455,13 @@ def main(args: argparse.Namespace) -> None:
418455
},
419456
})
420457

458+
if style in ("all", "optional", "free", "lucide"):
459+
processLucide(workDir, iconsDir, {
460+
"lucide": {
461+
"name": "Lucide",
462+
},
463+
})
464+
421465
if style in ("all", "optional", "non_free", "remix"):
422466
processRemix(workDir, iconsDir, {
423467
"remix_outline": {

utils/icon_themes/lucide.json

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
{
2+
"alert_error": "shield-alert",
3+
"alert_info": "info",
4+
"alert_question": "message-circle-question-mark",
5+
"alert_warn": "triangle-alert",
6+
7+
"cls_archive": "archive",
8+
"cls_character": "users",
9+
"cls_custom": "star",
10+
"cls_entity": "university",
11+
"cls_none": "square",
12+
"cls_novel": "book",
13+
"cls_object": "hammer",
14+
"cls_plot": "hat-glasses",
15+
"cls_template": "book-dashed",
16+
"cls_timeline": "history",
17+
"cls_trash": "trash-2",
18+
"cls_world": "earth",
19+
20+
"prj_folder": "folder",
21+
"prj_document": "file-text",
22+
"prj_title": "file-text",
23+
"prj_chapter": "file-text",
24+
"prj_scene": "file-text",
25+
"prj_note": "file-check",
26+
27+
"fmt_bold": "bold",
28+
"fmt_italic": "italic",
29+
"fmt_mark": "highlighter",
30+
"fmt_strike": "strikethrough",
31+
"fmt_subscript": "subscript",
32+
"fmt_superscript": "superscript",
33+
"fmt_underline": "underline",
34+
"fmt_toolbar": "panel-left-dashed",
35+
36+
"search": "search",
37+
"search_cancel": "x",
38+
"search_case": "case-sensitive",
39+
"search_loop": "repeat-2",
40+
"search_preserve": "a-large-small",
41+
"search_project": "book-search",
42+
"search_regex": "regex",
43+
"search_replace": "replace",
44+
"search_word": "whole-word",
45+
46+
"bullet-off": "circle",
47+
"bullet-on": "circle-check-big",
48+
"unfold-hide": "chevron-right",
49+
"unfold-show": "chevron-down",
50+
51+
"sb_build": "blocks",
52+
"sb_details": "view",
53+
"sb_novel": "book",
54+
"sb_outline": "layout-list",
55+
"sb_project": "folder-tree",
56+
"sb_search": "book-search",
57+
"sb_stats": "chart-bar-big",
58+
59+
"theme_light": "sun",
60+
"theme_dark": "moon",
61+
"theme_auto": "contrast",
62+
63+
"btn_ok": "circle-check-big",
64+
"btn_cancel": "circle-slash",
65+
"btn_yes": "circle-check-big",
66+
"btn_no": "circle-minus",
67+
"btn_open": "square-arrow-out-up-right",
68+
"btn_close": "circle-x",
69+
"btn_save": "save",
70+
"btn_browse": "folder-open",
71+
"btn_list": "list",
72+
"btn_new": "circle-plus",
73+
"btn_create": "sparkles",
74+
"btn_reset": "rotate-ccw",
75+
"btn_insert": "import",
76+
"btn_apply": "square-check-big",
77+
"btn_build": "blocks",
78+
"btn_print": "printer",
79+
"btn_preview": "eye",
80+
81+
"add": "plus",
82+
"bookmarks": "bookmark",
83+
"browse": "folder-open",
84+
"build_settings": "file-cog",
85+
"cancel": "circle-slash",
86+
"checked": "circle-check-big",
87+
"chevron_down": "arrow-down",
88+
"chevron_left": "arrow-left",
89+
"chevron_right": "arrow-right",
90+
"chevron_up": "arrow-up",
91+
"close": "x",
92+
"copy": "copy",
93+
"document_add": "file-plus-corner",
94+
"document": "bookmark",
95+
"edit": "pencil",
96+
"exclude": "ban",
97+
"export": "file-output",
98+
"filter": "funnel",
99+
"fit_height": "chevrons-up-down",
100+
"fit_width": "chevrons-left-right",
101+
"folder": "folder",
102+
"font": "type",
103+
"import": "file-input",
104+
"language": "languages",
105+
"lines": "list-ordered",
106+
"list": "list",
107+
"margin_bottom": "arrow-down-to-line",
108+
"margin_left": "arrow-left-to-line",
109+
"margin_right": "arrow-right-to-line",
110+
"margin_top": "arrow-up-to-line",
111+
"maximise": "expand",
112+
"minimise": "minimize",
113+
"more_arrow": "chevron-right",
114+
"more_vertical": "ellipsis-vertical",
115+
"noncheckable": "circle",
116+
"panel": "panels-left-bottom",
117+
"pin": "pin",
118+
"project_copy": "book-copy",
119+
"quote": "quote",
120+
"refresh": "refresh-cw",
121+
"remove": "minus",
122+
"revert": "undo-2",
123+
"settings": "settings",
124+
"stats": "chart-area",
125+
"text": "file-text",
126+
"timer_off": "timer-off",
127+
"timer": "timer",
128+
"unchecked": "circle-x",
129+
"view": "eye"
130+
}

utils/icon_themes/remix.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
"fit_height": "expand-height-line",
100100
"fit_width": "expand-width-line",
101101
"folder": "folder-6",
102-
"font": "font-sans",
102+
"font": "font-sans-serif",
103103
"import": "import",
104104
"language": "translate-2",
105105
"lines": "menu-2",

0 commit comments

Comments
 (0)