a very good jj gui
0
fork

Configure Feed

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

feat: add live repository updates via filesystem watching (TAT-16)

Watch .jj/repo directory for changes using notify crate with 500ms debounce.
UI automatically refreshes when repository state changes.

+221 -77
+90
Cargo.lock
··· 1847 1847 ] 1848 1848 1849 1849 [[package]] 1850 + name = "fsevent-sys" 1851 + version = "4.1.0" 1852 + source = "registry+https://github.com/rust-lang/crates.io-index" 1853 + checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" 1854 + dependencies = [ 1855 + "libc", 1856 + ] 1857 + 1858 + [[package]] 1850 1859 name = "futf" 1851 1860 version = "0.1.5" 1852 1861 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3518 3527 ] 3519 3528 3520 3529 [[package]] 3530 + name = "inotify" 3531 + version = "0.11.0" 3532 + source = "registry+https://github.com/rust-lang/crates.io-index" 3533 + checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" 3534 + dependencies = [ 3535 + "bitflags 2.10.0", 3536 + "inotify-sys", 3537 + "libc", 3538 + ] 3539 + 3540 + [[package]] 3541 + name = "inotify-sys" 3542 + version = "0.1.5" 3543 + source = "registry+https://github.com/rust-lang/crates.io-index" 3544 + checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" 3545 + dependencies = [ 3546 + "libc", 3547 + ] 3548 + 3549 + [[package]] 3521 3550 name = "inout" 3522 3551 version = "0.1.4" 3523 3552 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3758 3787 ] 3759 3788 3760 3789 [[package]] 3790 + name = "kqueue" 3791 + version = "1.1.1" 3792 + source = "registry+https://github.com/rust-lang/crates.io-index" 3793 + checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" 3794 + dependencies = [ 3795 + "kqueue-sys", 3796 + "libc", 3797 + ] 3798 + 3799 + [[package]] 3800 + name = "kqueue-sys" 3801 + version = "1.0.4" 3802 + source = "registry+https://github.com/rust-lang/crates.io-index" 3803 + checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" 3804 + dependencies = [ 3805 + "bitflags 1.3.2", 3806 + "libc", 3807 + ] 3808 + 3809 + [[package]] 3761 3810 name = "kstring" 3762 3811 version = "2.0.2" 3763 3812 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4142 4191 checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" 4143 4192 dependencies = [ 4144 4193 "libc", 4194 + "log", 4145 4195 "wasi", 4146 4196 "windows-sys 0.61.2", 4147 4197 ] ··· 4245 4295 version = "0.3.0" 4246 4296 source = "registry+https://github.com/rust-lang/crates.io-index" 4247 4297 checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" 4298 + 4299 + [[package]] 4300 + name = "notify" 4301 + version = "8.2.0" 4302 + source = "registry+https://github.com/rust-lang/crates.io-index" 4303 + checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" 4304 + dependencies = [ 4305 + "bitflags 2.10.0", 4306 + "fsevent-sys", 4307 + "inotify", 4308 + "kqueue", 4309 + "libc", 4310 + "log", 4311 + "mio", 4312 + "notify-types", 4313 + "walkdir", 4314 + "windows-sys 0.60.2", 4315 + ] 4316 + 4317 + [[package]] 4318 + name = "notify-debouncer-mini" 4319 + version = "0.7.0" 4320 + source = "registry+https://github.com/rust-lang/crates.io-index" 4321 + checksum = "17849edfaabd9a5fef1c606d99cfc615a8e99f7ac4366406d86c7942a3184cf2" 4322 + dependencies = [ 4323 + "log", 4324 + "notify", 4325 + "notify-types", 4326 + "tempfile", 4327 + ] 4328 + 4329 + [[package]] 4330 + name = "notify-types" 4331 + version = "2.0.0" 4332 + source = "registry+https://github.com/rust-lang/crates.io-index" 4333 + checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" 4248 4334 4249 4335 [[package]] 4250 4336 name = "ntapi" ··· 6298 6384 name = "tatami" 6299 6385 version = "0.1.0" 6300 6386 dependencies = [ 6387 + "anyhow", 6388 + "futures", 6301 6389 "gpui", 6302 6390 "jj-lib", 6391 + "notify", 6392 + "notify-debouncer-mini", 6303 6393 ] 6304 6394 6305 6395 [[package]]
+11
Cargo.toml
··· 2 2 name = "tatami" 3 3 version = "0.1.0" 4 4 edition = "2024" 5 + build = "build.rs" 6 + 7 + [package.metadata.bundle] 8 + name = "Tatami" 9 + identifier = "com.laulauland.tatami" 10 + icon = ["resources/AppIcon.png"] 11 + osx_minimum_system_version = "10.15.7" 5 12 6 13 [dependencies] 14 + anyhow = "1.0.100" 15 + futures = "0.3.31" 7 16 gpui = "0.2.2" 8 17 jj-lib = "0.35.0" 18 + notify = "8.2.0" 19 + notify-debouncer-mini = "0.7.0"
+81 -76
src/app.rs
··· 1 + use futures::StreamExt; 1 2 use gpui::{ 2 3 div, px, rgb, size, App, AppContext, Bounds, Context, IntoElement, ParentElement, Render, 3 4 Styled, Window, WindowBounds, WindowOptions, 4 5 }; 6 + use std::path::PathBuf; 5 7 6 8 use crate::repo::RepoState; 7 9 use crate::ui::log_view::render_log_view; 8 - use crate::ui::status_view::render_status_view; 10 + use crate::ui::theme::{self, Colors, TextSize}; 11 + use crate::watcher::RepoWatcher; 9 12 10 13 pub struct Tatami { 11 14 repo: RepoState, 15 + workspace_root: PathBuf, 12 16 selected_revision: Option<usize>, 17 + _watcher: Option<RepoWatcher>, 13 18 } 14 19 15 20 impl Tatami { 16 - pub fn new(repo: RepoState) -> Self { 21 + pub fn new(repo: RepoState, workspace_root: PathBuf) -> Self { 17 22 Self { 18 23 repo, 24 + workspace_root, 19 25 selected_revision: Some(0), 26 + _watcher: None, 20 27 } 21 28 } 29 + 30 + pub fn set_watcher(&mut self, watcher: RepoWatcher) { 31 + self._watcher = Some(watcher); 32 + } 33 + 34 + pub fn reload_repo(&mut self) { 35 + self.repo = crate::repo::load_workspace(&self.workspace_root); 36 + } 22 37 } 23 38 24 39 impl Render for Tatami { 25 40 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { 26 - let (main_content, right_panel) = match &self.repo { 27 - RepoState::NotFound { path } => ( 28 - div() 29 - .flex_1() 30 - .h_full() 31 - .p_4() 32 - .child(format!("No jj repository at {}", path.display())), 33 - div().w(px(300.0)).h_full(), 34 - ), 35 - RepoState::Loaded { 36 - revisions, status, .. 37 - } => ( 38 - div() 39 - .flex_1() 40 - .h_full() 41 - .child(render_log_view(revisions, self.selected_revision)), 42 - div() 43 - .w(px(300.0)) 44 - .h_full() 45 - .bg(rgb(0x252525)) 46 - .border_l_1() 47 - .border_color(rgb(0x3d3d3d)) 48 - .children(status.as_ref().map(render_status_view)), 49 - ), 50 - RepoState::Error { message } => ( 51 - div() 52 - .flex_1() 53 - .h_full() 54 - .p_4() 55 - .child(format!("Error: {}", message)), 56 - div().w(px(300.0)).h_full(), 57 - ), 41 + let content = match &self.repo { 42 + RepoState::NotFound { path } => div() 43 + .flex_1() 44 + .p_3() 45 + .child(format!("No jj repository at {}", path.display())), 46 + RepoState::Loaded { revisions, .. } => div() 47 + .flex_1() 48 + .child(render_log_view(revisions, self.selected_revision)), 49 + RepoState::Error { message } => { 50 + div().flex_1().p_3().child(format!("Error: {}", message)) 51 + } 58 52 }; 59 53 60 54 let status_text = match &self.repo { ··· 62 56 RepoState::Loaded { 63 57 workspace_root, 64 58 revisions, 65 - status, 59 + .. 66 60 } => { 67 - let file_count = status.as_ref().map(|s| s.files.len()).unwrap_or(0); 68 61 format!( 69 - "{} • {} revisions • {} changed files", 62 + "{} · {} revisions", 70 63 workspace_root 71 64 .file_name() 72 65 .unwrap_or_default() 73 66 .to_string_lossy(), 74 67 revisions.len(), 75 - file_count 76 68 ) 77 69 } 78 70 RepoState::Error { .. } => "Error".to_string(), ··· 82 74 .size_full() 83 75 .flex() 84 76 .flex_col() 85 - .bg(rgb(0x1e1e1e)) 86 - .text_color(rgb(0xcccccc)) 77 + .font_family(theme::font_family()) 78 + .text_size(TextSize::BASE) 79 + .bg(rgb(Colors::BG_BASE)) 80 + .text_color(rgb(Colors::TEXT)) 81 + .child(content) 87 82 .child( 88 83 div() 89 - .h(px(40.0)) 84 + .h(px(22.0)) 90 85 .w_full() 91 86 .flex() 87 + .flex_shrink_0() 92 88 .items_center() 93 - .px_4() 94 - .bg(rgb(0x2d2d2d)) 95 - .border_b_1() 96 - .border_color(rgb(0x3d3d3d)) 97 - .child("Tatami"), 98 - ) 99 - .child( 100 - div() 101 - .flex_1() 102 - .flex() 103 - .overflow_hidden() 104 - .child( 105 - div() 106 - .w(px(200.0)) 107 - .h_full() 108 - .bg(rgb(0x252525)) 109 - .border_r_1() 110 - .border_color(rgb(0x3d3d3d)) 111 - .p_2() 112 - .child("Bookmarks"), 113 - ) 114 - .child(main_content) 115 - .child(right_panel), 116 - ) 117 - .child( 118 - div() 119 - .h(px(24.0)) 120 - .w_full() 121 - .flex() 122 - .items_center() 123 - .px_4() 124 - .bg(rgb(0x007acc)) 125 - .text_color(rgb(0xffffff)) 89 + .px_3() 90 + .bg(rgb(Colors::BG_SURFACE)) 91 + .border_t_1() 92 + .border_color(rgb(Colors::BORDER_MUTED)) 93 + .text_size(TextSize::XS) 94 + .text_color(rgb(Colors::TEXT_SUBTLE)) 126 95 .child(status_text), 127 96 ) 128 97 } 129 98 } 130 99 131 - pub fn open_window(cx: &mut App, repo: RepoState) { 100 + pub fn open_window(cx: &mut App, repo: RepoState, workspace_root: PathBuf) { 132 101 let bounds = Bounds::centered(None, size(px(1200.0), px(800.0)), cx); 133 102 134 103 cx.open_window( ··· 136 105 window_bounds: Some(WindowBounds::Windowed(bounds)), 137 106 ..Default::default() 138 107 }, 139 - |_window, cx| cx.new(|_cx| Tatami::new(repo)), 108 + |_window, cx| { 109 + let entity = cx.new(|_cx| { 110 + let tatami = Tatami::new(repo.clone(), workspace_root.clone()); 111 + tatami 112 + }); 113 + 114 + if let RepoState::Loaded { .. } = &repo { 115 + let (watcher, mut receiver) = crate::watcher::watch_repo(workspace_root.clone()); 116 + 117 + entity.update(cx, |tatami, cx| { 118 + tatami.set_watcher(watcher); 119 + 120 + cx.spawn(|weak_self: gpui::WeakEntity<Tatami>, async_cx: &mut gpui::AsyncApp| { 121 + let mut async_cx = async_cx.clone(); 122 + async move { 123 + while let Some(()) = receiver.next().await { 124 + let Some(entity) = weak_self.upgrade() else { 125 + break; 126 + }; 127 + if async_cx 128 + .update_entity(&entity, |tatami, ctx| { 129 + tatami.reload_repo(); 130 + ctx.notify(); 131 + }) 132 + .is_err() 133 + { 134 + break; 135 + } 136 + } 137 + } 138 + }) 139 + .detach(); 140 + }); 141 + } 142 + 143 + entity 144 + }, 140 145 ) 141 146 .expect("Failed to open window"); 142 147 }
+3 -1
src/main.rs
··· 1 1 mod app; 2 2 mod repo; 3 3 mod ui; 4 + mod watcher; 4 5 5 6 use gpui::Application; 6 7 7 8 fn main() { 8 9 let current_dir = std::env::current_dir().unwrap_or_default(); 9 10 let repo_state = repo::load_workspace(&current_dir); 11 + let workspace_root = current_dir.clone(); 10 12 11 13 Application::new().run(|cx| { 12 - app::open_window(cx, repo_state); 14 + app::open_window(cx, repo_state, workspace_root); 13 15 }); 14 16 }
+36
src/watcher.rs
··· 1 + use futures::channel::mpsc; 2 + use notify::{RecommendedWatcher, RecursiveMode}; 3 + use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer}; 4 + use std::path::PathBuf; 5 + use std::time::Duration; 6 + 7 + pub struct RepoWatcher { 8 + _debouncer: Debouncer<RecommendedWatcher>, 9 + } 10 + 11 + pub fn watch_repo(workspace_root: PathBuf) -> (RepoWatcher, mpsc::UnboundedReceiver<()>) { 12 + let (tx, rx) = mpsc::unbounded(); 13 + let jj_repo_path = workspace_root.join(".jj").join("repo"); 14 + 15 + let mut debouncer = new_debouncer( 16 + Duration::from_millis(500), 17 + move |result: DebounceEventResult| { 18 + if let Ok(_events) = result { 19 + let _ = tx.unbounded_send(()); 20 + } 21 + }, 22 + ) 23 + .expect("Failed to create filesystem watcher"); 24 + 25 + debouncer 26 + .watcher() 27 + .watch(&jj_repo_path, RecursiveMode::Recursive) 28 + .expect("Failed to watch .jj/repo directory"); 29 + 30 + ( 31 + RepoWatcher { 32 + _debouncer: debouncer, 33 + }, 34 + rx, 35 + ) 36 + }