icons.rs
⎇
Raw
1//! Central icon registry — every glyph in the app comes from here.
2//!
3//! All icons are Material Symbols (filled, 24x24) fetched from
4//! <https://iconify.design> (set `material-symbols`) via the API at
5//! <https://api.iconify.design>. Each variant's doc comment names the
6//! source glyph, so swapping any icon is a one-file change: replace the
7//! body (raw SVG inner markup of a 24x24 grid, filled with
8//! `currentColor`) with the new one from the same API.
9
10use api_types::FileKind;
11
12#[derive(Clone, Copy, PartialEq, Eq, Debug)]
13pub enum IconName {
14 /// material-symbols:folder
15 Folder,
16 /// material-symbols:folder-open
17 FolderOpen,
18 /// material-symbols:folder-off
19 FolderOff,
20 /// material-symbols:description
21 File,
22 /// material-symbols:image
23 Image,
24 /// material-symbols:video-file
25 Video,
26 /// material-symbols:audio-file
27 Audio,
28 /// material-symbols:article
29 Text,
30 /// material-symbols:code
31 Code,
32 /// material-symbols:folder-zip
33 Archive,
34 /// material-symbols:picture-as-pdf
35 Pdf,
36 /// material-symbols:grid-view
37 Grid,
38 /// material-symbols:view-list
39 List,
40 /// material-symbols:refresh
41 Refresh,
42 /// material-symbols:upload-file
43 Upload,
44 /// material-symbols:drive-folder-upload
45 UploadFolder,
46 /// material-symbols:create-new-folder
47 NewFolder,
48 /// material-symbols:download
49 Download,
50 /// material-symbols:open-in-full
51 Open,
52 /// material-symbols:drive-file-rename
53 Rename,
54 /// material-symbols:drive-file-move
55 Move,
56 /// material-symbols:content-copy
57 Copy,
58 /// material-symbols:link
59 Share,
60 /// material-symbols:info
61 Info,
62 /// material-symbols:delete
63 Delete,
64 /// material-symbols:open-in-new
65 OpenInNew,
66 /// material-symbols:logout
67 Logout,
68 /// material-symbols:menu
69 Menu,
70 /// material-symbols:close
71 Close,
72 /// material-symbols:settings
73 Settings,
74 /// material-symbols:person
75 User,
76 /// material-symbols:admin-panel-settings
77 Admin,
78 /// material-symbols:add
79 Add,
80 /// material-symbols:brightness-auto
81 ThemeAuto,
82 /// material-symbols:light-mode
83 ThemeLight,
84 /// material-symbols:dark-mode
85 ThemeDark,
86}
87
88impl IconName {
89 /// Raw SVG body for the 24x24 grid, rendered inside
90 /// `<svg viewBox="0 0 24 24" fill="currentColor">`.
91 pub const fn body(self) -> &'static str {
92 match self {
93 Self::Folder => {
94 r#"<path fill="currentColor" d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h6l2 2h8q.825 0 1.413.588T22 8v10q0 .825-.587 1.413T20 20z"/>"#
95 }
96 Self::FolderOpen => {
97 r#"<path fill="currentColor" d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h6l2 2h8q.825 0 1.413.588T22 8H4v10l2.4-8h17.1l-2.575 8.575q-.2.65-.737 1.038T19 20z"/>"#
98 }
99 Self::FolderOff => {
100 r#"<path fill="currentColor" d="M21.775 18.95L6.85 4H9.7l2 2H20q.825 0 1.413.588T22 8v10q0 .275-.05.513t-.175.437m-1.3 4.35l-3.3-3.3H4q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4l2 2H3.2L.7 3.5l1.4-1.4l19.8 19.8z"/>"#
101 }
102 Self::File => {
103 r#"<path fill="currentColor" d="M8 18h8v-2H8zm0-4h8v-2H8zm-2 8q-.825 0-1.412-.587T4 20V4q0-.825.588-1.412T6 2h8l6 6v12q0 .825-.587 1.413T18 22zm7-13h5l-5-5z"/>"#
104 }
105 Self::Image => {
106 r#"<path fill="currentColor" d="M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm1-4h12l-3.75-5l-3 4L9 13z"/>"#
107 }
108 Self::Video => {
109 r#"<path fill="currentColor" d="M9 18h4q.425 0 .713-.288T14 17v-1l2 1.05v-4.1L14 14v-1q0-.425-.288-.712T13 12H9q-.425 0-.712.288T8 13v4q0 .425.288.713T9 18m-3 4q-.825 0-1.412-.587T4 20V4q0-.825.588-1.412T6 2h8l6 6v12q0 .825-.587 1.413T18 22zm7-13h5l-5-5z"/>"#
110 }
111 Self::Audio => {
112 r#"<path fill="currentColor" d="M10.75 19q.95 0 1.6-.65t.65-1.6V13h3v-2h-4v3.875q-.275-.2-.587-.288t-.663-.087q-.95 0-1.6.65t-.65 1.6t.65 1.6t1.6.65M6 22q-.825 0-1.412-.587T4 20V4q0-.825.588-1.412T6 2h8l6 6v12q0 .825-.587 1.413T18 22zm7-13h5l-5-5z"/>"#
113 }
114 Self::Text => {
115 r#"<path fill="currentColor" d="M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm2-4h7v-2H7zm0-4h10v-2H7zm0-4h10V7H7z"/>"#
116 }
117 Self::Code => {
118 r#"<path fill="currentColor" d="m8 18l-6-6l6-6l1.425 1.425l-4.6 4.6L9.4 16.6zm8 0l-1.425-1.425l4.6-4.6L14.6 7.4L16 6l6 6z"/>"#
119 }
120 Self::Archive => {
121 r#"<path fill="currentColor" d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h6l2 2h8q.825 0 1.413.588T22 8v10q0 .825-.587 1.413T20 20zm10-2h2v-2h2v-2h-2v-2h2v-2h-2V8h-2v2h2v2h-2v2h2v2h-2z"/>"#
122 }
123 Self::Pdf => {
124 r#"<path fill="currentColor" d="M9 12.5h1v-2h1q.425 0 .713-.288T12 9.5v-1q0-.425-.288-.712T11 7.5H9zm1-3v-1h1v1zm3 3h2q.425 0 .713-.288T16 11.5v-3q0-.425-.288-.712T15 7.5h-2zm1-1v-3h1v3zm3 1h1v-2h1v-1h-1v-1h1v-1h-2zM8 18q-.825 0-1.412-.587T6 16V4q0-.825.588-1.412T8 2h12q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18zm-4 4q-.825 0-1.412-.587T2 20V6h2v14h14v2z"/>"#
125 }
126 Self::Grid => {
127 r#"<path fill="currentColor" d="M3 11V3h8v8zm0 10v-8h8v8zm10-10V3h8v8zm0 10v-8h8v8z"/>"#
128 }
129 Self::List => {
130 r#"<path fill="currentColor" d="M9 20h11q.825 0 1.413-.587T22 18v-2H9zM2 8h5V4H4q-.825 0-1.412.588T2 6zm0 6h5v-4H2zm2 6h3v-4H2v2q0 .825.588 1.413T4 20m5-6h13v-4H9zm0-6h13V6q0-.825-.587-1.412T20 4H9z"/>"#
131 }
132 Self::Refresh => {
133 r#"<path fill="currentColor" d="M12 20q-3.35 0-5.675-2.325T4 12t2.325-5.675T12 4q1.725 0 3.3.712T18 6.75V4h2v7h-7V9h4.2q-.8-1.4-2.187-2.2T12 6Q9.5 6 7.75 7.75T6 12t1.75 4.25T12 18q1.925 0 3.475-1.1T17.65 14h2.1q-.7 2.65-2.85 4.325T12 20"/>"#
134 }
135 Self::Upload => {
136 r#"<path fill="currentColor" d="M11 19h2v-4.175l1.6 1.6L16 15l-4-4l-4 4l1.425 1.4L11 14.825zm-5 3q-.825 0-1.412-.587T4 20V4q0-.825.588-1.412T6 2h8l6 6v12q0 .825-.587 1.413T18 22zm7-13h5l-5-5z"/>"#
137 }
138 Self::UploadFolder => {
139 r#"<path fill="currentColor" d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h6l2 2h8q.825 0 1.413.588T22 8v10q0 .825-.587 1.413T20 20zm7-3h2v-4.2l1.6 1.6L16 13l-4-4l-4 4l1.4 1.4l1.6-1.6z"/>"#
140 }
141 Self::NewFolder => {
142 r#"<path fill="currentColor" d="M14 16h2v-2h2v-2h-2v-2h-2v2h-2v2h2zM4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h6l2 2h8q.825 0 1.413.588T22 8v10q0 .825-.587 1.413T20 20z"/>"#
143 }
144 Self::Download => {
145 r#"<path fill="currentColor" d="m12 16l-5-5l1.4-1.45l2.6 2.6V4h2v8.15l2.6-2.6L17 11zm-6 4q-.825 0-1.412-.587T4 18v-3h2v3h12v-3h2v3q0 .825-.587 1.413T18 20z"/>"#
146 }
147 Self::Open => {
148 r#"<path fill="currentColor" d="M3 21v-8h2v4.6L17.6 5H13V3h8v8h-2V6.4L6.4 19H11v2z"/>"#
149 }
150 Self::Rename => {
151 r#"<path fill="currentColor" d="M11.225 17H18v-2h-4.775zM6 17h3.075l6.5-6.5q.225-.225.325-.513t.1-.562t-.112-.55t-.313-.5l-.925-.925q-.225-.225-.5-.337T13.575 7q-.275 0-.55.1t-.5.325L6 13.925zm7.575-6.6l-.95-.95l.95-.95l.925.925zM5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21z"/>"#
152 }
153 Self::Move => {
154 r#"<path fill="currentColor" d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h6l2 2h8q.825 0 1.413.588T22 8v10q0 .825-.587 1.413T20 20zm8.2-6l-1.625 1.625l1.4 1.4L16 13l-4.025-4.025l-1.4 1.4L12.2 12H8v2z"/>"#
155 }
156 Self::Copy => {
157 r#"<path fill="currentColor" d="M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm-4 4q-.825 0-1.412-.587T3 20V6h2v14h11v2z"/>"#
158 }
159 Self::Share => {
160 r#"<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z"/>"#
161 }
162 Self::Info => {
163 r#"<path fill="currentColor" d="M11 17h2v-6h-2zm1.713-8.287Q13 8.425 13 8t-.288-.712T12 7t-.712.288T11 8t.288.713T12 9t.713-.288M12 22q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22"/>"#
164 }
165 Self::Delete => {
166 r#"<path fill="currentColor" d="M7 21q-.825 0-1.412-.587T5 19V6H4V4h5V3h6v1h5v2h-1v13q0 .825-.587 1.413T17 21zm2-4h2V8H9zm4 0h2V8h-2z"/>"#
167 }
168 Self::OpenInNew => {
169 r#"<path fill="currentColor" d="M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h7v2H5v14h14v-7h2v7q0 .825-.587 1.413T19 21zm4.7-5.3l-1.4-1.4L17.6 5H14V3h7v7h-2V6.4z"/>"#
170 }
171 Self::Logout => {
172 r#"<path fill="currentColor" d="M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h7v2H5v14h7v2zm11-4l-1.375-1.45l2.55-2.55H9v-2h8.175l-2.55-2.55L16 7l5 5z"/>"#
173 }
174 Self::Menu => {
175 r#"<path fill="currentColor" d="M3 18v-2h18v2zm0-5v-2h18v2zm0-5V6h18v2z"/>"#
176 }
177 Self::Close => {
178 r#"<path fill="currentColor" d="M6.4 19L5 17.6l5.6-5.6L5 6.4L6.4 5l5.6 5.6L17.6 5L19 6.4L13.4 12l5.6 5.6l-1.4 1.4l-5.6-5.6z"/>"#
179 }
180 Self::Settings => {
181 r#"<path fill="currentColor" d="m9.25 22l-.4-3.2q-.325-.125-.612-.3t-.563-.375L4.7 19.375l-2.75-4.75l2.575-1.95Q4.5 12.5 4.5 12.338v-.675q0-.163.025-.338L1.95 9.375l2.75-4.75l2.975 1.25q.275-.2.575-.375t.6-.3l.4-3.2h5.5l.4 3.2q.325.125.613.3t.562.375l2.975-1.25l2.75 4.75l-2.575 1.95q.025.175.025.338v.674q0 .163-.05.338l2.575 1.95l-2.75 4.75l-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2zm2.8-6.5q1.45 0 2.475-1.025T15.55 12t-1.025-2.475T12.05 8.5q-1.475 0-2.488 1.025T8.55 12t1.013 2.475T12.05 15.5"/>"#
182 }
183 Self::User => {
184 r#"<path fill="currentColor" d="M9.175 10.825Q8 9.65 8 8t1.175-2.825T12 4t2.825 1.175T16 8t-1.175 2.825T12 12t-2.825-1.175M4 20v-2.8q0-.85.438-1.562T5.6 14.55q1.55-.775 3.15-1.162T12 13t3.25.388t3.15 1.162q.725.375 1.163 1.088T20 17.2V20z"/>"#
185 }
186 Self::Admin => {
187 r#"<path fill="currentColor" d="M17 22q-2.075 0-3.537-1.463T12 17t1.463-3.537T17 12t3.538 1.463T22 17t-1.463 3.538T17 22m-5 0q-3.475-.875-5.738-3.988T4 11.1V5l8-3l8 3v5.675q-.65-.325-1.463-.5T17 10q-2.9 0-4.95 2.05T10 17q0 1.55.588 2.8t1.487 2.175q-.025 0-.037.013T12 22m6.063-5.437q.437-.438.437-1.063t-.437-1.062T17 14t-1.062.438T15.5 15.5t.438 1.063T17 17t1.063-.437M17 20q.775 0 1.425-.363t1.05-.962q-.55-.325-1.175-.5T17 18t-1.3.175t-1.175.5q.4.6 1.05.963T17 20"/>"#
188 }
189 Self::Add => r#"<path fill="currentColor" d="M11 13H5v-2h6V5h2v6h6v2h-6v6h-2z"/>"#,
190 Self::ThemeAuto => {
191 r#"<path fill="currentColor" d="M7.8 16h1.6l.8-2.3h3.65l.8 2.3h1.55l-3.4-9h-1.6zm2.85-3.6l1.3-3.75h.1l1.3 3.75zM12 23.3L8.65 20H4v-4.65L.7 12L4 8.65V4h4.65L12 .7L15.35 4H20v4.65L23.3 12L20 15.35V20h-4.65z"/>"#
192 }
193 Self::ThemeLight => {
194 r#"<path fill="currentColor" d="M8.463 15.538Q7 14.075 7 12t1.463-3.537T12 7t3.538 1.463T17 12t-1.463 3.538T12 17t-3.537-1.463M5 13H1v-2h4zm18 0h-4v-2h4zM11 5V1h2v4zm0 18v-4h2v4zM6.4 7.75L3.875 5.325L5.3 3.85l2.4 2.5zm12.3 12.4l-2.425-2.525L17.6 16.25l2.525 2.425zM16.25 6.4l2.425-2.525L20.15 5.3l-2.5 2.4zM3.85 18.7l2.525-2.425L7.75 17.6l-2.425 2.525z"/>"#
195 }
196 Self::ThemeDark => {
197 r#"<path fill="currentColor" d="M12 21q-3.75 0-6.375-2.625T3 12t2.625-6.375T12 3q.35 0 .688.025t.662.075q-1.025.725-1.638 1.888T11.1 7.5q0 2.25 1.575 3.825T16.5 12.9q1.375 0 2.525-.613T20.9 10.65q.05.325.075.662T21 12q0 3.75-2.625 6.375T12 21"/>"#
198 }
199 }
200 }
201}
202
203/// File types that get the "code" icon instead of the plain-text one. Both are
204/// [`FileKind::Text`] to the server — code-vs-prose is a naming question, not
205/// a content one, so it stays here.
206const CODE_EXTS: &[&str] = &[
207 "rs", "js", "mjs", "cjs", "ts", "tsx", "jsx", "py", "rb", "go", "c", "cc", "cpp", "h", "hpp",
208 "java", "kt", "cs", "php", "swift", "lua", "sh", "bash", "fish", "zsh", "ps1", "json", "toml",
209 "yaml", "yml", "html", "htm", "css", "scss", "sql", "xml", "vue", "svelte",
210];
211
212/// Pick the icon for an entry from its server-sniffed [`FileKind`].
213///
214/// `name` is used only to split [`FileKind::Text`] into prose and code.
215pub fn icon_for(kind: FileKind, name: &str) -> IconName {
216 match kind {
217 FileKind::Dir => IconName::Folder,
218 FileKind::Image => IconName::Image,
219 FileKind::Video => IconName::Video,
220 FileKind::Audio => IconName::Audio,
221 FileKind::Pdf => IconName::Pdf,
222 FileKind::Archive => IconName::Archive,
223 FileKind::Text => {
224 let ext = name.rsplit('.').next().unwrap_or("").to_lowercase();
225 if CODE_EXTS.contains(&ext.as_str()) {
226 IconName::Code
227 } else {
228 IconName::Text
229 }
230 }
231 FileKind::Binary => IconName::File,
232 }
233}
234
235impl IconName {
236 /// Folder-shaped glyphs are tinted with the accent color in the browser.
237 pub const fn is_folder(self) -> bool {
238 matches!(self, Self::Folder | Self::FolderOpen)
239 }
240}
241