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:note-add
49 NewFile,
50 /// material-symbols:download
51 Download,
52 /// material-symbols:open-in-full
53 Open,
54 /// material-symbols:drive-file-rename
55 Rename,
56 /// material-symbols:drive-file-move
57 Move,
58 /// material-symbols:content-copy
59 Copy,
60 /// material-symbols:link
61 Share,
62 /// material-symbols:info
63 Info,
64 /// material-symbols:delete
65 Delete,
66 /// material-symbols:open-in-new
67 OpenInNew,
68 /// material-symbols:logout
69 Logout,
70 /// material-symbols:menu
71 Menu,
72 /// material-symbols:close
73 Close,
74 /// material-symbols:settings
75 Settings,
76 /// material-symbols:search
77 Search,
78 /// material-symbols:stop
79 Stop,
80 /// material-symbols:person
81 User,
82 /// material-symbols:admin-panel-settings
83 Admin,
84 /// material-symbols:add
85 Add,
86 /// material-symbols:brightness-auto
87 ThemeAuto,
88 /// material-symbols:light-mode
89 ThemeLight,
90 /// material-symbols:dark-mode
91 ThemeDark,
92 /// material-symbols:more-vert
93 MoreVert,
94}
95
96impl IconName {
97 /// Raw SVG body for the 24x24 grid, rendered inside
98 /// `<svg viewBox="0 0 24 24" fill="currentColor">`.
99 pub const fn body(self) -> &'static str {
100 match self {
101 Self::Folder => {
102 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"/>"#
103 }
104 Self::FolderOpen => {
105 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"/>"#
106 }
107 Self::FolderOff => {
108 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"/>"#
109 }
110 Self::File => {
111 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"/>"#
112 }
113 Self::Image => {
114 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"/>"#
115 }
116 Self::Video => {
117 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"/>"#
118 }
119 Self::Audio => {
120 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"/>"#
121 }
122 Self::Text => {
123 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"/>"#
124 }
125 Self::Code => {
126 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"/>"#
127 }
128 Self::Archive => {
129 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"/>"#
130 }
131 Self::Pdf => {
132 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"/>"#
133 }
134 Self::Grid => {
135 r#"<path fill="currentColor" d="M3 11V3h8v8zm0 10v-8h8v8zm10-10V3h8v8zm0 10v-8h8v8z"/>"#
136 }
137 Self::List => {
138 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"/>"#
139 }
140 Self::Refresh => {
141 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"/>"#
142 }
143 Self::Upload => {
144 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"/>"#
145 }
146 Self::UploadFolder => {
147 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"/>"#
148 }
149 Self::NewFolder => {
150 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"/>"#
151 }
152 Self::NewFile => {
153 r#"<path fill="currentColor" d="M11 18h2v-3h3v-2h-3v-3h-2v3H8v2h3zm-5 4q-.825 0-1.412-.587T4 20V4q0-.825.588-1.412T6 2h8l6 6v12q0 .825-.587 1.413T18 22zm7-13h5l-5-5z"/>"#
154 }
155 Self::Download => {
156 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"/>"#
157 }
158 Self::Open => {
159 r#"<path fill="currentColor" d="M3 21v-8h2v4.6L17.6 5H13V3h8v8h-2V6.4L6.4 19H11v2z"/>"#
160 }
161 Self::Rename => {
162 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"/>"#
163 }
164 Self::Move => {
165 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"/>"#
166 }
167 Self::Copy => {
168 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"/>"#
169 }
170 Self::Share => {
171 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"/>"#
172 }
173 Self::Info => {
174 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"/>"#
175 }
176 Self::Delete => {
177 r#"<path fill="currentColor" d="M7 21q-.825 0-1.412-.587T5 19V6H4V4h5V3h6v1h5v2h-1v13q0 .825-.587 1.413T17 21zm2-4h2V8H9zm4 0h2V8h-2z"/>"#
178 }
179 Self::OpenInNew => {
180 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"/>"#
181 }
182 Self::Logout => {
183 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"/>"#
184 }
185 Self::Menu => {
186 r#"<path fill="currentColor" d="M3 18v-2h18v2zm0-5v-2h18v2zm0-5V6h18v2z"/>"#
187 }
188 Self::Close => {
189 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"/>"#
190 }
191 Self::Settings => {
192 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"/>"#
193 }
194 Self::Search => {
195 r#"<path fill="currentColor" d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14"/>"#
196 }
197 Self::Stop => r#"<path fill="currentColor" d="M6 18V6h12v12z"/>"#,
198 Self::User => {
199 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"/>"#
200 }
201 Self::Admin => {
202 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"/>"#
203 }
204 Self::Add => r#"<path fill="currentColor" d="M11 13H5v-2h6V5h2v6h6v2h-6v6h-2z"/>"#,
205 Self::ThemeAuto => {
206 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"/>"#
207 }
208 Self::ThemeLight => {
209 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"/>"#
210 }
211 Self::ThemeDark => {
212 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"/>"#
213 }
214 Self::MoreVert => {
215 r#"<path fill="currentColor" d="M12 20q-.825 0-1.412-.587T10 18t.588-1.412T12 16t1.413.588T14 18t-.587 1.413T12 20m0-6q-.825 0-1.412-.587T10 12t.588-1.412T12 10t1.413.588T14 12t-.587 1.413T12 14m0-6q-.825 0-1.412-.587T10 6t.588-1.412T12 4t1.413.588T14 6t-.587 1.413T12 8"/>"#
216 }
217 }
218 }
219}
220
221/// File types that get the "code" icon instead of the plain-text one. Both are
222/// [`FileKind::Text`] to the server — code-vs-prose is a naming question, not
223/// a content one, so it stays here.
224const CODE_EXTS: &[&str] = &[
225 "rs", "js", "mjs", "cjs", "ts", "tsx", "jsx", "py", "rb", "go", "c", "cc", "cpp", "h", "hpp",
226 "java", "kt", "cs", "php", "swift", "lua", "sh", "bash", "fish", "zsh", "ps1", "json", "toml",
227 "yaml", "yml", "html", "htm", "css", "scss", "sql", "xml", "vue", "svelte",
228];
229
230/// Pick the icon for an entry from its server-sniffed [`FileKind`].
231///
232/// `name` is used only to split [`FileKind::Text`] into prose and code.
233pub fn icon_for(kind: FileKind, name: &str) -> IconName {
234 match kind {
235 FileKind::Dir => IconName::Folder,
236 FileKind::Image => IconName::Image,
237 FileKind::Video => IconName::Video,
238 FileKind::Audio => IconName::Audio,
239 FileKind::Pdf => IconName::Pdf,
240 FileKind::Archive => IconName::Archive,
241 FileKind::Text => {
242 let ext = name.rsplit('.').next().unwrap_or("").to_lowercase();
243 if CODE_EXTS.contains(&ext.as_str()) {
244 IconName::Code
245 } else {
246 IconName::Text
247 }
248 }
249 FileKind::Binary => IconName::File,
250 }
251}
252
253impl IconName {
254 /// Folder-shaped glyphs are tinted with the accent color in the browser.
255 pub const fn is_folder(self) -> bool {
256 matches!(self, Self::Folder | Self::FolderOpen)
257 }
258}
259