Personal outliner built with Rust.
4

Configure Feed

Select the types of activity you want to include in your feed.

ui-polish group 1: bundle Tabler icons behind an AssetSource, swap header arrows / calendar chevrons / calendar-button glyph to icons; icon() helper sets explicit tint since gpui Svg has no text-color cascade

graham.systems (Jul 15, 2026, 3:30 PM -0700) 1d065c01 3044ba85

+315 -27
+21
crates/trawler/assets/icons/LICENSE
··· 1 + MIT License 2 + 3 + Copyright (c) 2020-2026 Paweł Kuna 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+21
crates/trawler/assets/icons/arrow-left.svg
··· 1 + <!-- 2 + tags: [back, swipe, return, arrow, left, direction, pointer, navigation, flow, navigate] 3 + category: Arrows 4 + version: "1.0" 5 + unicode: "ea19" 6 + --> 7 + <svg 8 + xmlns="http://www.w3.org/2000/svg" 9 + width="24" 10 + height="24" 11 + viewBox="0 0 24 24" 12 + fill="none" 13 + stroke="currentColor" 14 + stroke-width="2" 15 + stroke-linecap="round" 16 + stroke-linejoin="round" 17 + > 18 + <path d="M5 12l14 0" /> 19 + <path d="M5 12l6 6" /> 20 + <path d="M5 12l6 -6" /> 21 + </svg>
+21
crates/trawler/assets/icons/arrow-right.svg
··· 1 + <!-- 2 + tags: [next, proceed, swipe, arrow, right, direction, pointer, navigation, flow, navigate] 3 + category: Arrows 4 + version: "1.0" 5 + unicode: "ea1f" 6 + --> 7 + <svg 8 + xmlns="http://www.w3.org/2000/svg" 9 + width="24" 10 + height="24" 11 + viewBox="0 0 24 24" 12 + fill="none" 13 + stroke="currentColor" 14 + stroke-width="2" 15 + stroke-linecap="round" 16 + stroke-linejoin="round" 17 + > 18 + <path d="M5 12l14 0" /> 19 + <path d="M13 18l6 -6" /> 20 + <path d="M13 6l6 6" /> 21 + </svg>
+24
crates/trawler/assets/icons/calendar.svg
··· 1 + <!-- 2 + tags: [date, day, plan, schedule, agenda, calender, calendar, control, operation, function] 3 + category: System 4 + version: "1.0" 5 + unicode: "ea53" 6 + --> 7 + <svg 8 + xmlns="http://www.w3.org/2000/svg" 9 + width="24" 10 + height="24" 11 + viewBox="0 0 24 24" 12 + fill="none" 13 + stroke="currentColor" 14 + stroke-width="2" 15 + stroke-linecap="round" 16 + stroke-linejoin="round" 17 + > 18 + <path d="M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12" /> 19 + <path d="M16 3v4" /> 20 + <path d="M8 3v4" /> 21 + <path d="M4 11h16" /> 22 + <path d="M11 15h1" /> 23 + <path d="M12 15v3" /> 24 + </svg>
+19
crates/trawler/assets/icons/chevron-left.svg
··· 1 + <!-- 2 + tags: [move, previous, back, chevron, left, navigation, flow, movement, route, path] 3 + category: Arrows 4 + version: "1.0" 5 + unicode: "ea60" 6 + --> 7 + <svg 8 + xmlns="http://www.w3.org/2000/svg" 9 + width="24" 10 + height="24" 11 + viewBox="0 0 24 24" 12 + fill="none" 13 + stroke="currentColor" 14 + stroke-width="2" 15 + stroke-linecap="round" 16 + stroke-linejoin="round" 17 + > 18 + <path d="M15 6l-6 6l6 6" /> 19 + </svg>
+19
crates/trawler/assets/icons/chevron-right.svg
··· 1 + <!-- 2 + tags: [move, checklist, next, chevron, right, navigation, flow, movement, route, path] 3 + category: Arrows 4 + version: "1.0" 5 + unicode: "ea61" 6 + --> 7 + <svg 8 + xmlns="http://www.w3.org/2000/svg" 9 + width="24" 10 + height="24" 11 + viewBox="0 0 24 24" 12 + fill="none" 13 + stroke="currentColor" 14 + stroke-width="2" 15 + stroke-linecap="round" 16 + stroke-linejoin="round" 17 + > 18 + <path d="M9 6l6 6l-6 6" /> 19 + </svg>
+21
crates/trawler/assets/icons/layout-sidebar-right-collapse.svg
··· 1 + <!-- 2 + tags: [grid, aside, column, columns, menu, navigation, layout, sidebar, right, collapse] 3 + category: Design 4 + version: "1.53" 5 + unicode: "f006" 6 + --> 7 + <svg 8 + xmlns="http://www.w3.org/2000/svg" 9 + width="24" 10 + height="24" 11 + viewBox="0 0 24 24" 12 + fill="none" 13 + stroke="currentColor" 14 + stroke-width="2" 15 + stroke-linecap="round" 16 + stroke-linejoin="round" 17 + > 18 + <path d="M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2l0 -12" /> 19 + <path d="M15 4v16" /> 20 + <path d="M9 10l2 2l-2 2" /> 21 + </svg>
+21
crates/trawler/assets/icons/layout-sidebar-right-expand.svg
··· 1 + <!-- 2 + tags: [grid, aside, column, columns, menu, navigation, layout, sidebar, right, expand] 3 + category: Design 4 + version: "1.53" 5 + unicode: "f007" 6 + --> 7 + <svg 8 + xmlns="http://www.w3.org/2000/svg" 9 + width="24" 10 + height="24" 11 + viewBox="0 0 24 24" 12 + fill="none" 13 + stroke="currentColor" 14 + stroke-width="2" 15 + stroke-linecap="round" 16 + stroke-linejoin="round" 17 + > 18 + <path d="M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2l0 -12" /> 19 + <path d="M15 4v16" /> 20 + <path d="M10 10l-2 2l2 2" /> 21 + </svg>
+109
crates/trawler/src/assets.rs
··· 1 + //! Embedded UI assets (openspec change ui-polish, capability app-chrome): 2 + //! the bundled Tabler icons, compiled into the binary and served to gpui's 3 + //! `svg()` element through an [`AssetSource`] — the same spirit as the 4 + //! bundled fonts: no system dependencies, identical rendering everywhere. 5 + //! 6 + //! Icons are the MIT-licensed Tabler outline set 7 + //! (<https://tabler.io/icons>); see `assets/icons/LICENSE`. Only the icons 8 + //! the UI actually uses are bundled — add to [`ICONS`] when a new one is 9 + //! needed. 10 + 11 + use std::borrow::Cow; 12 + 13 + use gpui::{AssetSource, Result, SharedString}; 14 + 15 + /// Every bundled icon: the path `svg().path(...)` refers to it by, and its 16 + /// embedded bytes. 17 + const ICONS: &[(&str, &[u8])] = &[ 18 + ( 19 + "icons/arrow-left.svg", 20 + include_bytes!("../assets/icons/arrow-left.svg"), 21 + ), 22 + ( 23 + "icons/arrow-right.svg", 24 + include_bytes!("../assets/icons/arrow-right.svg"), 25 + ), 26 + ( 27 + "icons/calendar.svg", 28 + include_bytes!("../assets/icons/calendar.svg"), 29 + ), 30 + ( 31 + "icons/chevron-left.svg", 32 + include_bytes!("../assets/icons/chevron-left.svg"), 33 + ), 34 + ( 35 + "icons/chevron-right.svg", 36 + include_bytes!("../assets/icons/chevron-right.svg"), 37 + ), 38 + ( 39 + "icons/layout-sidebar-right-collapse.svg", 40 + include_bytes!("../assets/icons/layout-sidebar-right-collapse.svg"), 41 + ), 42 + ( 43 + "icons/layout-sidebar-right-expand.svg", 44 + include_bytes!("../assets/icons/layout-sidebar-right-expand.svg"), 45 + ), 46 + ]; 47 + 48 + /// The application's [`AssetSource`], registered via 49 + /// `Application::with_assets` at startup. 50 + pub struct Assets; 51 + 52 + impl AssetSource for Assets { 53 + fn load(&self, path: &str) -> Result<Option<Cow<'static, [u8]>>> { 54 + Ok(ICONS 55 + .iter() 56 + .find(|(p, _)| *p == path) 57 + .map(|(_, bytes)| Cow::Borrowed(*bytes))) 58 + } 59 + 60 + fn list(&self, path: &str) -> Result<Vec<SharedString>> { 61 + Ok(ICONS 62 + .iter() 63 + .filter(|(p, _)| p.starts_with(path)) 64 + .map(|(p, _)| (*p).into()) 65 + .collect()) 66 + } 67 + } 68 + 69 + /// A bundled Tabler icon sized to sit inline with UI text, tinted a neutral 70 + /// UI-chrome gray. gpui's `Svg` element paints only when a text color is set 71 + /// on the element itself (`Svg::paint` zips the path with the element's own 72 + /// `style.text.color` — there is no cascade for svg tinting), so the color 73 + /// must be explicit here; call sites can override it via `.text_color(...)`. 74 + pub fn icon(path: &'static str) -> gpui::Svg { 75 + use gpui::Styled as _; 76 + gpui::svg() 77 + .path(path) 78 + .size(gpui::px(16.0)) 79 + .flex_none() 80 + .text_color(gpui::rgb(0xd0d0d0)) 81 + } 82 + 83 + #[cfg(test)] 84 + mod tests { 85 + use super::*; 86 + 87 + #[test] 88 + fn every_registered_icon_loads_and_parses_as_svg() { 89 + for (path, _) in ICONS { 90 + let bytes = Assets 91 + .load(path) 92 + .unwrap() 93 + .unwrap_or_else(|| panic!("{path} must load")); 94 + let text = std::str::from_utf8(&bytes).expect("svg is utf-8"); 95 + assert!(text.contains("<svg"), "{path} does not look like an svg"); 96 + assert!( 97 + text.contains("currentColor"), 98 + "{path} must tint via currentColor (text color cascade)" 99 + ); 100 + } 101 + } 102 + 103 + #[test] 104 + fn list_returns_all_icons_under_the_icons_prefix() { 105 + assert_eq!(Assets.list("icons/").unwrap().len(), ICONS.len()); 106 + assert!(Assets.list("fonts/").unwrap().is_empty()); 107 + assert!(Assets.load("icons/nonexistent.svg").unwrap().is_none()); 108 + } 109 + }
+37 -25
crates/trawler/src/main.rs
··· 29 29 //! its backlinks. Backlinks are always shown below the main content for 30 30 //! any non-Journal view (spec: "Backlinks are always visible"). 31 31 32 + mod assets; 32 33 #[cfg(feature = "devtools")] 33 34 mod devtools; 34 35 mod editor; ··· 2085 2086 .id("calendar-prev") 2086 2087 .cursor_pointer() 2087 2088 .px_1() 2089 + .py_0p5() 2088 2090 .rounded_sm() 2089 2091 .hover(|d| d.bg(rgb(0x2a2a3a))) 2090 - .child("◀") 2092 + .child(assets::icon("icons/chevron-left.svg")) 2091 2093 .on_click(cx.listener(|this, _event: &ClickEvent, _window, cx| { 2092 2094 this.shift_calendar_month(-1, cx); 2093 2095 })), ··· 2102 2104 .id("calendar-next") 2103 2105 .cursor_pointer() 2104 2106 .px_1() 2107 + .py_0p5() 2105 2108 .rounded_sm() 2106 2109 .hover(|d| d.bg(rgb(0x2a2a3a))) 2107 - .child("▶") 2110 + .child(assets::icon("icons/chevron-right.svg")) 2108 2111 .on_click(cx.listener(|this, _event: &ClickEvent, _window, cx| { 2109 2112 this.shift_calendar_month(1, cx); 2110 2113 })), ··· 2755 2758 .id("nav-back") 2756 2759 .cursor_pointer() 2757 2760 .px_1() 2761 + .py_0p5() 2758 2762 .rounded_sm() 2759 2763 .hover(|d| d.bg(rgb(0x2a2a3a))) 2760 - .child("◀") 2764 + .child(assets::icon("icons/arrow-left.svg")) 2761 2765 .on_click(cx.listener(|this, _event: &ClickEvent, window, cx| { 2762 2766 this.navigate_back(&NavigateBack, window, cx); 2763 2767 })), ··· 2767 2771 .id("nav-forward") 2768 2772 .cursor_pointer() 2769 2773 .px_1() 2774 + .py_0p5() 2770 2775 .rounded_sm() 2771 2776 .hover(|d| d.bg(rgb(0x2a2a3a))) 2772 - .child("▶") 2777 + .child(assets::icon("icons/arrow-right.svg")) 2773 2778 .on_click(cx.listener(|this, _event: &ClickEvent, window, cx| { 2774 2779 this.navigate_forward(&NavigateForward, window, cx); 2775 2780 })), ··· 2794 2799 .px_2() 2795 2800 .rounded_sm() 2796 2801 .hover(|d| d.bg(rgb(0x2a2a3a))) 2802 + .flex() 2803 + .flex_row() 2804 + .items_center() 2805 + .gap_1() 2806 + .child(assets::icon("icons/calendar.svg")) 2797 2807 .child("Calendar") 2798 2808 .on_click(cx.listener( 2799 2809 |this, _event: &ClickEvent, window, cx| { ··· 3073 3083 } 3074 3084 3075 3085 let graph_dir = default_graph_dir(); 3076 - Application::new().run(move |cx: &mut App| { 3077 - load_bundled_fonts(cx); 3078 - editor::init(cx); 3079 - init_keymap(cx); 3080 - let bounds = Bounds::centered(None, size(px(900.0), px(600.0)), cx); 3081 - let open_dir = graph_dir.clone(); 3082 - let window = cx 3083 - .open_window( 3084 - WindowOptions { 3085 - window_bounds: Some(WindowBounds::Windowed(bounds)), 3086 - ..Default::default() 3087 - }, 3088 - move |window, cx| cx.new(|cx| TrawlerApp::new(open_dir.clone(), window, cx)), 3089 - ) 3090 - .unwrap(); 3091 - #[cfg(feature = "devtools")] 3092 - devtools::maybe_start(graph_dir, window, cx); 3093 - #[cfg(not(feature = "devtools"))] 3094 - let _ = window; 3095 - cx.activate(true); 3096 - }); 3086 + Application::new() 3087 + .with_assets(assets::Assets) 3088 + .run(move |cx: &mut App| { 3089 + load_bundled_fonts(cx); 3090 + editor::init(cx); 3091 + init_keymap(cx); 3092 + let bounds = Bounds::centered(None, size(px(900.0), px(600.0)), cx); 3093 + let open_dir = graph_dir.clone(); 3094 + let window = cx 3095 + .open_window( 3096 + WindowOptions { 3097 + window_bounds: Some(WindowBounds::Windowed(bounds)), 3098 + ..Default::default() 3099 + }, 3100 + move |window, cx| cx.new(|cx| TrawlerApp::new(open_dir.clone(), window, cx)), 3101 + ) 3102 + .unwrap(); 3103 + #[cfg(feature = "devtools")] 3104 + devtools::maybe_start(graph_dir, window, cx); 3105 + #[cfg(not(feature = "devtools"))] 3106 + let _ = window; 3107 + cx.activate(true); 3108 + }); 3097 3109 }
+2 -2
openspec/changes/ui-polish/tasks.md
··· 1 1 ## 1. Icons and asset plumbing 2 2 3 - - [ ] 1.1 Add `AssetSource` registration at app start; bundle selected Tabler SVGs under `crates/trawler/assets/icons/` with the MIT license file 4 - - [ ] 1.2 Replace the ◀/▶ header glyphs with Tabler arrow icons (tinted via text color); add the calendar icon and the sidebar-toggle pair (`layout-sidebar-right-collapse` / `layout-sidebar-right-expand`) 3 + - [x] 1.1 Add `AssetSource` registration at app start (`assets::Assets`, `Application::with_assets`); bundled 7 Tabler outline SVGs under `crates/trawler/assets/icons/` with the MIT license file; unit tests assert every registered icon loads, parses as SVG, and tints via `currentColor` 4 + - [x] 1.2 Replace the ◀/▶ glyphs with Tabler icons — header back/forward use arrow-left/right, calendar month-nav uses chevron-left/right, the Calendar button gained the calendar icon; sidebar-toggle pair (`layout-sidebar-right-collapse` / `-expand`) bundled for group 3. Implementation note: gpui's `Svg` paints only with an *explicit* element text color (no cascade — `Svg::paint` zips path with the element's own `style.text.color`), so `assets::icon()` sets the tint (`0xd0d0d0`), overridable per call site. Verified visually via dev-loop screenshots (header + open calendar) 5 5 6 6 ## 2. Window chrome (design D1) 7 7