use std::{io, path::Path, sync::Arc}; use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}; use jj_lib::{ commit::Commit, config::StackedConfig, graph::{GraphEdge, GraphEdgeType, TopoGroupedGraphIterator}, repo::{Repo as _, StoreFactories}, revset::{RevsetExpression, RevsetIteratorExt, UserRevsetExpression}, settings::UserSettings, workspace::{WorkingCopyFactories, Workspace}, }; use ratatui::{ DefaultTerminal, Frame, buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Style, Stylize}, symbols::border, text::{Line, Text}, widgets::{Block, Borders, Paragraph, Widget}, }; #[derive(Debug, Default)] pub struct App { counter: u8, exit: bool, } // Plan // // 1. run jj-log, get commits between mega-merge and each history's base. // 2. show UI where they are rendered separately // 3. make more consice UI (where other histories will be collapsed) impl App { pub fn run(mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { while !self.exit { // terminal.draw(|frame| self.draw(frame))?; terminal.draw(|frame| frame.render_widget(&self, frame.area()))?; self.handle_events()?; } Ok(()) } fn draw(&self, frame: &mut Frame) { // TODO: draw all UI let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(3), Constraint::Min(1), Constraint::Length(3), ]) .split(frame.area()); let title_block = Block::default() .borders(Borders::ALL) .style(Style::default()); let title_text = Text::from("hey"); // let title_text = Text::from(vec![ // Line::from(vec![ // "Value: ".into(), // self.counter.to_string().yellow(), // ]), // ]); let title = Paragraph::new(title_text) .block(title_block); frame.render_widget(title, chunks[0]); // frame.render_widget(self, frame.area()); } fn handle_events(&mut self) -> io::Result<()> { match event::read()? { Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { self.handle_key_event(key_event) } _ => {} }; Ok(()) } fn handle_key_event(&mut self, key_event: KeyEvent) { match key_event.code { KeyCode::Char('q') => self.exit(), KeyCode::Char('h') | KeyCode::Left => self.decrement_counter(), KeyCode::Char('l') | KeyCode::Right => self.increment_counter(), _ => {} } } fn exit(&mut self) { self.exit = true } fn increment_counter(&mut self) { self.counter += 1; } fn decrement_counter(&mut self) { self.counter = self.counter.saturating_sub(1) } } impl Widget for &App { fn render(self, area: Rect, buf: &mut Buffer) { let title = Line::from(" Counter App Tutorial ".bold()); let block = Block::bordered() .title(title.centered()) .border_set(border::THICK); let counter_text = Text::from(vec![Line::from(vec![ "Value: ".into(), self.counter.to_string().yellow(), ])]); Paragraph::new(counter_text) .centered() .block(block) .render(area, buf); } } mod jj_helper; use jj_cli::{cli_util::CommandHelper, formatter::FormatterExt as _, graphlog::SaplingGraphLog}; use jj_cli::cli_util::RevisionArg; use jj_cli::command_error::CommandError; use jj_cli::ui::Ui; use jj_cli::{ cli_util::{CliRunner, LogContentFormat, format_template}, graphlog::{GraphStyle, get_graphlog}, templater::TemplateRenderer, }; use renderdag::GraphRowRenderer; // fn main() -> anyhow::Result<()> { // let path = Path::new("/Users/boltless/repo/tangled"); // let user_settings = UserSettings::from_config(StackedConfig::with_defaults())?; // let store_factories = StoreFactories::default(); // let working_copy_factories = WorkingCopyFactories::new(); // let ws = Workspace::load( // &user_settings, // path, // &store_factories, // &working_copy_factories, // )?; // name = ws.workspace_name(); // // let mut terminal = ratatui::init(); // // let result = App::default().run(&mut terminal); // // ratatui::restore(); // // result // Ok(()) // } #[derive(clap::Parser, Clone, Debug)] enum CustomCommand { MegaLog(MegaLogArgs), } /// Frobnicate a revisions #[derive(clap::Args, Clone, Debug)] struct MegaLogArgs { /// The revision to frobnicate #[arg(default_value = "@")] revision: RevisionArg, } fn run_custom_command( ui: &mut Ui, command_helper: &CommandHelper, command: CustomCommand, ) -> Result<(), CommandError> { match command { CustomCommand::MegaLog(args) => mega_merge_log(ui, command_helper, &args), } } fn mega_merge_log( ui: &mut Ui, command_helper: &CommandHelper, _args: &MegaLogArgs, ) -> Result<(), CommandError> { // 1. from "MM", get all edges, // 2. render graphs based on those edges, similar logic to jj-log // choose template based on focus state let workspace_command = command_helper.workspace_helper(ui)?; let settings = workspace_command.settings(); let repo = workspace_command.repo(); let store = repo.store(); let graph_style = GraphStyle::from_settings(settings)?; let use_elided_nodes = settings.get_bool("ui.log-synthetic-elided-nodes")?; let with_content_format = LogContentFormat::new(ui, settings)?; let template: TemplateRenderer; let node_template: TemplateRenderer>; { let language = workspace_command.commit_template_language(); let template_string = settings.get_string("templates.log")?; template = workspace_command .parse_template(ui, &language, &template_string)? .labeled(["log", "commit"]); node_template = workspace_command .parse_template(ui, &language, &settings.get_string("templates.log_node")?)? .labeled(["log", "commit", "node"]); } // this part will be configurable let mm_rev = RevisionArg::from(String::from("MM")); let mm_commit = workspace_command.resolve_single_rev(ui, &mm_rev)?; let immutable_exp = { let rev = String::from("immutable()"); workspace_command.parse_revset(ui, &RevisionArg::from(rev))? }; let trunk_exp = { let rev = String::from("trunk()"); workspace_command.parse_revset(ui, &RevisionArg::from(rev))? }; // let parent = mm_commit.parents().next().unwrap()?; for parent in mm_commit.parents().take(3) { let parent = parent?; println!("{}", parent.change_id()); // create revsets based on each branches // maybe make this configurable as "log_revs(head)"? // ancestors(::x ~ immutable(), 2) let exp = RevsetExpression::union( &RevsetExpression::ancestors_range( &RevsetExpression::minus( &RevsetExpression::ancestors(&RevsetExpression::commits(vec![parent.id().clone()])), immutable_exp.expression(), ), 0..2, ), // &RevsetExpression::commits(vec![mm_commit.id().clone()]), trunk_exp.expression(), ); let exp = workspace_command.attach_revset_evaluator(exp); let revset = exp.evaluate()?; let mut formatter = ui.stdout_formatter(); let formatter = formatter.as_mut(); let mut raw_output = formatter.raw()?; // let mut graph = get_graphlog(graph_style, raw_output.as_mut()); // let mut graph = GraphRowRenderer::new().output().with_min_row_height(0).build_box_drawing(); let mut graph = { let builder = GraphRowRenderer::new().output().with_min_row_height(0); SaplingGraphLog::create(builder.build_box_drawing().with_square_glyphs(), raw_output.as_mut()) }; let iter = { let mut forward_iter = TopoGroupedGraphIterator::new(revset.iter_graph(), |id| id); forward_iter.prioritize_branch(parent.id().clone()); forward_iter }; for node in iter { let (commit_id, edges) = node?; // The graph is keyed by (CommitId, is_synthetic) let mut graphlog_edges = vec![]; let mut missing_edge_id = None; let mut elided_targets = vec![]; for edge in edges { match edge.edge_type { GraphEdgeType::Missing => { missing_edge_id = Some(edge.target); } GraphEdgeType::Direct => { graphlog_edges.push(GraphEdge::direct((edge.target, false))); } GraphEdgeType::Indirect => { if use_elided_nodes { elided_targets.push(edge.target.clone()); graphlog_edges.push(GraphEdge::direct((edge.target, true))); } else { graphlog_edges.push(GraphEdge::indirect((edge.target, false))); } } } } if let Some(missing_edge_id) = missing_edge_id { graphlog_edges.push(GraphEdge::missing((missing_edge_id, false))); } let mut buffer = vec![]; let key = (commit_id, false); let commit = store.get_commit(&key.0)?; with_content_format .sub_width(graph.width(&key, &graphlog_edges)) .write(ui.new_formatter(&mut buffer).as_mut(), |formatter| { template.format(&commit, formatter) })?; if !buffer.ends_with(b"\n") { buffer.push(b'\n'); } let node_symbol = format_template(ui, &Some(commit), &node_template); graph.add_node( &key, &graphlog_edges, &node_symbol, &String::from_utf8_lossy(&buffer), )?; for elided_target in elided_targets { let elided_key = (elided_target, true); let real_key = (elided_key.0.clone(), false); let edges = [GraphEdge::direct(real_key)]; let mut buffer = vec![]; let within_graph = with_content_format.sub_width(graph.width(&elided_key, &edges)); within_graph.write(ui.new_formatter(&mut buffer).as_mut(), |formatter| { writeln!(formatter.labeled("elided"), "(elided revisions)") })?; let node_symbol = format_template(ui, &None, &node_template); graph.add_node( &elided_key, &edges, &node_symbol, &String::from_utf8_lossy(&buffer), )?; } } } // let revset_expression = { // // let rev = String::from("ancestors(@, 4)"); // let rev = String::from("ancestors(MM, 2)"); // workspace_command.parse_revset(ui, &RevisionArg::from(rev))? // }; // let revset = revset_expression.evaluate()?; // // so this is possible // let mut terminal = ratatui::init(); // let result = App::default().run(&mut terminal); // ratatui::restore(); // result?; Ok(()) } fn main() -> std::process::ExitCode { CliRunner::init() .add_subcommand(run_custom_command) .run() .into() } // fn main() -> anyhow::Result<()> { // let mut terminal = ratatui::init(); // let result = App::default().run(&mut terminal); // ratatui::restore(); // result?; // Ok(()) // }