big view refactor into a new crate

[?]
May 14, 2025, 1:28 PM
23SFYK4Q5NKBPJG53PQNPWQH6UOUU2YKJEL7RLXYBRLJOJYV7AWQC

Dependencies

  • [2] 6YZAVBWU Initial commit
  • [3] IQDCHWCP load a pijul repo
  • [4] SWWE2R6M display basic repo stuff
  • [5] WT3GA27P add cursor with selection
  • [6] DVKSPF7R track selected file path together with an index
  • [7] UB2ITZJS refresh changed files on FS changes
  • [8] EC3TVL4X add untracked files
  • [9] KT5UYXGK fix selection after adding file, add changed file diffs
  • [10] ELG3UDT6 allow to rm added files
  • [11] S2NVIFXR allow to enter record msg
  • [12] W7IUT3ZV start recording impl
  • [13] YBJRDOTC make all repo actions async
  • [14] KM5PSZ4A watch repo once loaded
  • [15] 2VUX5BTD load identity
  • [16] A5YBC77V record!
  • [17] D7A7MSIH allow to defer or abandon record, add buttons
  • [18] UCBNZULE make changed files paths optional (no path for root)
  • [19] 4WO3ZJM2 show untracked files' contents
  • [20] BJXUYQ2Y show untracked file contents in read-only text editor
  • [21] CFYW3HGZ wip: display changed files
  • [22] W4LFX7IH group diffs by file name
  • [23] PTFDJ567 add untracked files encoding
  • [24] AMPZ2BXK show changed files diffs (only Edit atm)
  • [25] FDDPOH5R add arrow controls
  • [26] NOB64XMR fmt and clippy
  • [27] AXSXZQDG fix updating changed file contents, styling
  • [28] V55EAIWQ add src file LRU cache
  • [29] Y5ATDI2H convert changed file diffs and load src only if any needs it
  • [30] UMO6U2ZT partition the change files diffs on whether they have content
  • [31] 6SW7UVSH update iced version
  • [32] B4RMW5AE add syntax highlighter to untracked files contents
  • [33] MJDGPSHG WIP contents diff
  • [34] ZVI4AWER woot contents_diff
  • [35] QMAUTRB6 refactor diff
  • [36] VUIRSTKH fmt
  • [37] OQ6HSAWH show record log
  • [38] NWJD6VM6 mv libflowers libflorescence
  • [39] AHWWRC73 navigate log entries
  • [40] UJPRF6DA fix log changes selection
  • [41] TEI5NQ3S add log files selection
  • [42] DCSUCH6R add undecoded diff view, improve decoded view style
  • [43] O7PQIOJ3 add missing undecoded diffs views
  • [44] UUB7SHLR more re-use in diffs
  • [45] JE44NYHM display log files diffs
  • [46] 4ELJZGRJ load and store all change diffs at once
  • [47] HC7ROIBC move main diffs state out of cursor
  • [48] FR52XEMW add action for log change file diff
  • [49] L6KSEFQI move cursor related stuff into its module
  • [50] BFN2VHZS refactor file stuff into sub-mod
  • [51] VJNWIGSX clippy
  • [52] GWZGYNIB add view crate
  • [53] 3SYSJKYL add app icon
  • [54] CALXOZXA flatten crates dir
  • [55] Z2CJPWZE focus record message text_editor on spawn
  • [56] WI2BVQ6J rm client lib crate
  • [57] HOJZI52Y rename flowers_ui to inflorescence
  • [58] NRCUG4R2 load changed files src when selected

Change contents

  • file addition: util.rs (----------)
    [52.85]
    use iced::Element;
    /// A short-named wrapper into `Element`
    #[inline(always)]
    pub fn el<'a, E, M>(e: E) -> Element<'a, M>
    where
    E: Into<Element<'a, M>>,
    {
    Into::<Element<M>>::into(e)
    }
  • replacement in inflorescence_view/src/lib.rs at line 1
    [52.102][52.103:122]()
    use iced::Element;
    [52.102]
    [52.122]
    pub mod app;
    pub mod diff;
    mod util;
  • replacement in inflorescence_view/src/lib.rs at line 5
    [52.123][52.123:295]()
    /// A short-named wrapper into `Element`
    #[inline(always)]
    pub fn el<'a, E, M>(e: E) -> Element<'a, M>
    where
    E: Into<Element<'a, M>>,
    {
    Into::<Element<M>>::into(e)
    [52.123]
    [52.295]
    use iced::{window, Theme};
    pub use util::el;
    pub fn theme<S>(_state: &S, _window_id: window::Id) -> Theme {
    Theme::TokyoNight
  • file addition: diff.rs (----------)
    [52.85]
    use std::cmp;
    use iced::widget::{column, container, row, text};
    use iced::{alignment, Background, Color, Element, Font, Length};
    use crate::el;
    // TODO: maybe use theme
    const DELETED_BG_COLOR: Color = Color::from_rgba8(190, 37, 40, 0.15);
    const ADDED_BG_COLOR: Color = Color::from_rgba8(47, 148, 11, 0.15);
    #[derive(Debug, Default)]
    pub struct State {
    pub selected_sections: Vec<usize>,
    pub expanded_unchanged_sections: Vec<usize>,
    pub collapsed_changed_sections: Vec<usize>,
    }
    #[derive(Debug, Clone)]
    pub enum Msg {}
    /// [`File`] is not part of [`State`] so it can be stored separately (i.e. in a
    /// cache, where it's immutable once set, unlike [`State`] which can change with
    /// [`Action`]s)
    #[derive(Debug)]
    pub enum File {
    Decoded(DecodedFile),
    Undecodable(UndecodableFile),
    }
    #[derive(Debug)]
    pub struct DecodedFile {
    pub combined: Combined,
    pub diffs_without_contents: Vec<DiffWithoutContents>,
    }
    #[derive(Debug)]
    pub struct UndecodableFile {
    pub diffs_with_contents: Vec<DiffWithContents>,
    pub diffs_without_contents: Vec<DiffWithoutContents>,
    }
    /// A file combined with its diffs into sections
    #[derive(Debug)]
    pub struct Combined {
    pub sections: Vec<Section>,
    pub max_line_num: usize,
    }
    #[derive(Debug)]
    pub enum DiffWithContents {
    Add,
    Edit {
    line: usize,
    deleted: bool,
    contents: String,
    },
    Replacement {
    line: usize,
    /// Deleted line
    change_contents: String,
    /// Added lines
    replacement_contents: String,
    },
    Del,
    Undel,
    }
    #[derive(Debug)]
    pub enum DiffWithoutContents {
    // _________________________________________________________________________
    // Cases that never have contents:
    Move,
    SolveNameConflict,
    UnsolveNameConflict,
    SolveOrderConflict,
    UnsolveOrderConflict,
    ResurrectZombines,
    AddRoot,
    DelRoot,
    // _________________________________________________________________________
    // Cases that normally have contents, but in these cases the contents are
    // not decodable:
    Edit {
    line: usize,
    deleted: bool,
    contents: UndecodableContents,
    },
    Replacement {
    line: usize,
    /// Deleted line
    change_contents: UndecodableContents,
    /// Added lines
    replacement_contents: UndecodableContents,
    },
    }
    #[derive(Debug)]
    pub enum UndecodableContents {
    /// Short byte sequence of unknown encoding encoded with base64 for
    /// display. Must be shorter than [`crate::repo::MAX_LEN_BASE64_DISPLAY`]
    ShortBase64(String),
    UnknownEncoding,
    }
    #[derive(Debug)]
    pub enum Section {
    Unchanged(Lines),
    /// `deleted` and `added` are together because for
    /// `ChangedFileDiffWithContents::Replacement` they begin on the same line
    /// number
    Changed {
    deleted: Lines,
    added: Lines,
    },
    }
    /// INVARIANT: There must be no new-lines in any of the strings, the source
    /// string must be split on those.
    pub type Lines = Vec<String>;
    pub fn view<'a>(state: Option<&'a State>, file: &'a File) -> Element<'a, Msg> {
    match file {
    File::Decoded(decoded_file) => view_decoded(state, decoded_file),
    File::Undecodable(undecodable_file) => {
    view_undecodable(state, undecodable_file)
    }
    }
    }
    pub fn contents_to_lines(contents: &str) -> Lines {
    contents.split('\n').map(str::to_string).collect()
    }
    fn view_decoded<'a>(
    _state: Option<&'a State>,
    file: &'a DecodedFile,
    ) -> Element<'a, Msg> {
    let DecodedFile {
    combined,
    diffs_without_contents,
    } = file;
    let line_num_digits = combined.max_line_num.to_string().len();
    // TODO use state to display selection, and control section expansion
    let mut current_line = 1;
    let sections_view = combined.sections.iter().map(|section| match section {
    Section::Unchanged(lines) => {
    let res = lines.iter().enumerate().map(move |(ix, line)| {
    line_view(
    LineKind::Unchanged,
    current_line + ix,
    line_num_digits,
    line,
    )
    });
    current_line += lines.len();
    el(column(res))
    }
    Section::Changed { deleted, added } => {
    let res = deleted
    .iter()
    .enumerate()
    .map(move |(ix, line)| {
    line_view(
    LineKind::Deleted,
    current_line + ix,
    line_num_digits,
    line,
    )
    })
    .chain(added.iter().enumerate().map(move |(ix, line)| {
    line_view(
    LineKind::Added,
    current_line + ix,
    line_num_digits,
    line,
    )
    }));
    current_line += added.len();
    el(column(res))
    }
    });
    if diffs_without_contents.is_empty() {
    el(column(sections_view))
    } else {
    let diffs_without_contents_view = diffs_without_contents
    .iter()
    .map(view_diff_without_contents);
    el(column([
    el(column(diffs_without_contents_view)),
    el(column(sections_view)),
    ])
    .spacing(10))
    }
    }
    fn view_undecodable<'a>(
    _state: Option<&'a State>,
    file: &'a UndecodableFile,
    ) -> Element<'a, Msg> {
    let UndecodableFile {
    diffs_with_contents,
    diffs_without_contents,
    } = file;
    let diffs = diffs_with_contents
    .iter()
    .map(view_diff_with_contents)
    .chain(
    diffs_without_contents
    .iter()
    .map(view_diff_without_contents),
    );
    el(column(diffs).spacing(10))
    }
    /// View diffs without context (the file contents)
    fn view_diff_with_contents(diff: &DiffWithContents) -> Element<'_, Msg> {
    match diff {
    DiffWithContents::Add => el(text("Added")),
    DiffWithContents::Edit {
    line,
    deleted,
    contents,
    } => {
    let line_num = *line;
    let lines = contents_to_lines(contents);
    let max_line_num = line_num + lines.len();
    let line_num_digits = max_line_num.to_string().len();
    let lines_view = lines.into_iter().enumerate().map(|(ix, line)| {
    line_view(
    if *deleted {
    LineKind::Deleted
    } else {
    LineKind::Added
    },
    line_num + ix,
    line_num_digits,
    line,
    )
    });
    el(column(lines_view))
    }
    DiffWithContents::Replacement {
    line,
    change_contents,
    replacement_contents,
    } => {
    let line_num = *line;
    let change_lines = contents_to_lines(change_contents);
    let replacement_lines = contents_to_lines(replacement_contents);
    let max_line_num = line_num
    + cmp::max(change_lines.len(), replacement_lines.len());
    let line_num_digits = max_line_num.to_string().len();
    let lines_view = change_lines
    .into_iter()
    .enumerate()
    .map(|(ix, line)| {
    line_view(
    LineKind::Deleted,
    line_num + ix,
    line_num_digits,
    line,
    )
    })
    .chain(replacement_lines.into_iter().enumerate().map(
    |(ix, line)| {
    line_view(
    LineKind::Added,
    line_num + ix,
    line_num_digits,
    line,
    )
    },
    ));
    el(column(lines_view))
    }
    DiffWithContents::Del => el(text("Deleted")),
    DiffWithContents::Undel => el(text("Revived")),
    }
    }
    /// View diffs without context (the file contents)
    fn view_diff_without_contents(diff: &DiffWithoutContents) -> Element<'_, Msg> {
    match diff {
    DiffWithoutContents::Move => el(text("Move")),
    DiffWithoutContents::SolveNameConflict => {
    el(text("Solve name conflict"))
    }
    DiffWithoutContents::UnsolveNameConflict => {
    el(text("Unsolve name conflict"))
    }
    DiffWithoutContents::SolveOrderConflict => {
    el(text("Solve order conflict"))
    }
    DiffWithoutContents::UnsolveOrderConflict => {
    el(text("Unsolve order conflict"))
    }
    DiffWithoutContents::ResurrectZombines => el(text("Resurrect zombies")),
    DiffWithoutContents::AddRoot => el(text("Add root")),
    DiffWithoutContents::DelRoot => el(text("Delete root")),
    DiffWithoutContents::Edit {
    line,
    deleted,
    contents,
    } => {
    let line_num = *line;
    let line = undecodable_contents_to_str(contents);
    line_view(
    if *deleted {
    LineKind::Deleted
    } else {
    LineKind::Added
    },
    line_num,
    1,
    line,
    )
    }
    DiffWithoutContents::Replacement {
    line,
    change_contents,
    replacement_contents,
    } => {
    let line_num = *line;
    let change_line = undecodable_contents_to_str(change_contents);
    let replacement_line =
    undecodable_contents_to_str(replacement_contents);
    el(column([
    line_view(LineKind::Deleted, line_num, 1, change_line),
    line_view(LineKind::Added, line_num, 1, replacement_line),
    ]))
    }
    }
    }
    fn mono_text<'a>(txt: impl text::IntoFragment<'a>) -> iced::widget::Text<'a> {
    text(txt)
    .font(Font::MONOSPACE)
    .wrapping(text::Wrapping::WordOrGlyph)
    .align_y(alignment::Vertical::Top)
    }
    fn line_num_view<'a>(num: usize, digits: usize) -> iced::widget::Text<'a> {
    // Fill the string to the number of digits
    let txt = format!("{num:digits$} ");
    mono_text(txt)
    .font(Font::MONOSPACE)
    .style(move |theme| {
    let palette = theme.extended_palette();
    text::Style {
    color: Some(palette.background.base.text.scale_alpha(0.61)),
    }
    })
    .align_y(alignment::Vertical::Top)
    }
    #[derive(Debug, Clone, Copy)]
    enum LineKind {
    Unchanged,
    Added,
    Deleted,
    }
    fn line_view<'a>(
    kind: LineKind,
    line_num: usize,
    line_num_digits: usize,
    line: impl text::IntoFragment<'a>,
    ) -> Element<'a, Msg> {
    let line = container(row([
    el(mono_text(match kind {
    LineKind::Unchanged => " ",
    LineKind::Added => "+ ",
    LineKind::Deleted => "- ",
    })),
    el(line_num_view(line_num, line_num_digits)),
    el(mono_text(line).width(Length::Fill)),
    ]));
    el(match kind {
    LineKind::Unchanged => line,
    LineKind::Added => line.style(|_theme| {
    container::background(Background::from(ADDED_BG_COLOR))
    }),
    LineKind::Deleted => line.style(|_theme| {
    container::background(Background::from(DELETED_BG_COLOR))
    }),
    })
    }
    fn undecodable_contents_to_str(contents: &UndecodableContents) -> &str {
    match contents {
    UndecodableContents::ShortBase64(short) => short,
    UndecodableContents::UnknownEncoding => "Unknown encoding",
    }
    }
  • file addition: app.rs (----------)
    [52.85]
    //! Main app view
    use crate::{diff, el};
    use iced::widget::{button, column, row, scrollable, text};
    use iced::{font, window, Border, Color, Element, Font, Length, Theme};
    use libflorescence::prelude::*;
    use libflorescence::repo;
    use iced::widget::text_editor;
    use std::collections::HashMap;
    use std::path::Path;
    const SPACING: u32 = 10;
    #[derive(Debug)]
    pub struct State<'a> {
    pub repo_path: &'a Path,
    pub repo: Option<&'a repo::State>,
    pub cursor: &'a cursor::State,
    pub record_msg: Option<&'a RecordMsg>,
    pub diffs_state: &'a HashMap<file::Id, diff::State>,
    }
    #[derive(Debug, Clone)]
    pub enum Msg {
    Cursor(cursor::Msg),
    ToRepo(repo::MsgIn),
    EditRecordMsg(text_editor::Action),
    DeferRecord,
    SaveRecord,
    AbandonRecord,
    FileDiffsContentsAction {
    id: file::Id,
    action: diff::Msg,
    },
    LogChangeFileDiffAction {
    hash: pijul::Hash,
    file: String,
    action: diff::Msg,
    },
    }
    pub mod file {
    use crate::diff;
    #[derive(Debug, Clone, Hash, PartialEq, Eq)]
    pub struct Id {
    pub path: String,
    pub file_kind: Kind,
    }
    #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
    pub enum Kind {
    Untracked,
    Changed,
    }
    #[derive(Debug)]
    pub enum Diff {
    Loading,
    Loaded(diff::File),
    }
    }
    pub mod cursor {
    use crate::diff;
    use libflorescence::prelude::pijul;
    use std::collections::HashMap;
    #[derive(Debug, Clone)]
    pub enum Msg {
    Down,
    Up,
    Right,
    Left,
    Select(Select),
    }
    #[derive(Debug, Default)]
    pub struct State {
    pub selection: Option<Selection>,
    }
    #[derive(Debug)]
    pub enum Selection {
    UntrackedFile {
    ix: usize,
    path: String,
    },
    ChangedFile {
    ix: usize,
    path: String,
    },
    LogChange {
    ix: usize,
    hash: pijul::Hash,
    message: String,
    /// All the diffs in this change keyed by file path. Loaded async
    /// and set to None only while loading. The
    /// `diff::State` is also in here so that is it
    /// preserved while navigating between files.
    diffs: Option<HashMap<String, (diff::File, diff::State)>>,
    file: Option<LogChangeFileSelection>,
    },
    }
    #[derive(Debug)]
    pub struct LogChangeFileSelection {
    pub ix: usize,
    pub path: String,
    }
    #[derive(Debug, Clone)]
    pub enum Select {
    UntrackedFile {
    ix: usize,
    path: String,
    },
    ChangedFile {
    ix: usize,
    path: String,
    },
    LogChange {
    ix: usize,
    hash: pijul::Hash,
    message: String,
    },
    LogChangeFile {
    ix: usize,
    path: String,
    },
    }
    }
    #[derive(Debug)]
    pub enum RecordMsg {
    Typing(text_editor::Content),
    Canceled { old_msg: String },
    }
    pub fn view<'a, F>(
    state: State<'a>,
    _window_id: window::Id,
    get_file_diff: F,
    ) -> Element<'a, Msg>
    where
    F: Fn(&file::Id) -> Option<&'a file::Diff>,
    {
    if let Some(repo) = state.repo.as_ref() {
    let repo_info = el(row([
    el(text(&repo.dir_name)),
    el(text(": ")),
    el(
    button(text(&repo.channel)), /* TODO
    * .on_press(Message) */
    ),
    ]));
    let untracked_files = || {
    el(column(repo.untracked_files.iter().enumerate().map(
    |(ix, path)| {
    let is_selected = matches!(state.cursor.selection.as_ref() ,
    Some(cursor::Selection::UntrackedFile{ ix: selected_ix, .. }) if &ix == selected_ix
    );
    el(
    button(text(path))
    .on_press(Msg::Cursor(cursor::Msg::Select(
    cursor::Select::UntrackedFile{ix, path: path.clone()},
    )))
    .style(selectable_button_style(is_selected)),
    )
    },
    )))
    };
    let changed_files = || {
    el(column(repo.changed_files.iter().enumerate().map(
    |(ix, (file_path, _diffs))| {
    let is_selected = matches!(state.cursor.selection.as_ref(),
    Some(cursor::Selection::ChangedFile{ ix: selected_ix, .. }) if &ix == selected_ix
    );
    el(
    button(
    text(file_path))
    .on_press(Msg::Cursor(cursor::Msg::Select(
    cursor::Select::ChangedFile{ix, path: file_path.clone()},
    )))
    .style(selectable_button_style(is_selected)),
    )
    },
    )))
    };
    let log = || {
    el(column(repo.log.iter().enumerate().map(
    |(ix, repo::LogEntry {
    hash,
    message,
    file_paths: _,
    })| {
    let short_hash = display_short_hash(hash);
    let is_selected = matches!(state.cursor.selection.as_ref(),
    Some(cursor::Selection::LogChange { ix: selected_ix, .. }) if &ix == selected_ix
    );
    el(row([
    el(button(text(short_hash).font(Font::MONOSPACE))
    .on_press(Msg::Cursor(cursor::Msg::Select(
    cursor::Select::LogChange { ix, hash: *hash, message: message.clone() },
    )))
    .style(selectable_button_style(is_selected))),
    el(text(message)),
    ])
    .spacing(SPACING))
    },
    )))
    };
    let record_msg_editor = if let Some(RecordMsg::Typing(msg_content)) =
    state.record_msg.as_ref()
    {
    el(column([
    el(text_editor(msg_content)
    .placeholder("Type something here...")
    .on_action(Msg::EditRecordMsg)),
    el(row([
    el(button(text("Save")).on_press(Msg::SaveRecord)),
    el(button(text("Defer")).on_press(Msg::DeferRecord)),
    el(button(text("Abandon")).on_press(Msg::AbandonRecord)),
    ])),
    ]))
    } else {
    el(row([]))
    };
    let selection_details = match state.cursor.selection.as_ref() {
    Some(cursor::Selection::UntrackedFile { ix: _, path }) => {
    let id = file::Id {
    path: path.clone(),
    file_kind: file::Kind::Untracked,
    };
    let diffs = match get_file_diff(&id) {
    Some(file::Diff::Loaded(file)) => {
    let selection_state = state.diffs_state.get(&id);
    diff::view(selection_state, file).map(move |msg| {
    Msg::FileDiffsContentsAction {
    id: id.clone(),
    action: msg,
    }
    })
    }
    None | Some(file::Diff::Loading) => {
    el(text("Loading diff..."))
    }
    };
    el(column([
    view_diff_header(format!(
    "Untracked file {path} contents:"
    )),
    el(scrollable(diffs)),
    ])
    .spacing(SPACING))
    }
    Some(cursor::Selection::ChangedFile { path, ix: _ }) => {
    let id = file::Id {
    path: path.clone(),
    file_kind: file::Kind::Changed,
    };
    let diffs = match get_file_diff(&id) {
    Some(file::Diff::Loaded(file)) => {
    let selection_state = state.diffs_state.get(&id);
    diff::view(selection_state, file).map(move |msg| {
    Msg::FileDiffsContentsAction {
    id: id.clone(),
    action: msg,
    }
    })
    }
    None | Some(file::Diff::Loading) => {
    el(text("Loading diff..."))
    }
    };
    el(column([
    view_diff_header(format!("Changed file {path} diff:")),
    el(scrollable(diffs)),
    ])
    .spacing(SPACING))
    }
    Some(cursor::Selection::LogChange {
    ix,
    hash,
    message,
    diffs: _,
    file,
    }) => {
    let entry = state.repo.as_ref().unwrap().log.get(*ix).unwrap();
    let short_hash = display_short_hash(hash);
    let files = entry.file_paths.iter().enumerate().map(|(ix, path)| {
    let is_selected = matches!(file, Some(cursor::LogChangeFileSelection{ path: selected_path, .. }) if selected_path == path);
    el(button(text(path)).on_press_with(move || {
    Msg::Cursor(cursor::Msg::Select(cursor::Select::LogChangeFile { ix, path: path.clone() }))
    }).style(selectable_button_style(is_selected)))
    });
    el(column([
    view_diff_header(format!("{short_hash} message:")),
    el(text(message)),
    view_diff_header("Changed files:".to_string()),
    el(scrollable(column(files))),
    ])
    .spacing(SPACING))
    }
    None => el(row([])),
    };
    let left_view = match state.cursor.selection.as_ref() {
    Some(cursor::Selection::LogChange {
    ix: _,
    hash,
    message: _,
    diffs,
    file: Some(cursor::LogChangeFileSelection { ix: _, path }),
    }) => el(column([
    view_diff_header(format!(
    "{path} changes in {}:",
    display_short_hash(hash)
    )),
    match diffs {
    Some(diffs) => {
    let (file, state) = diffs.get(path).unwrap();
    diff::view(Some(state), file).map(|action| {
    Msg::LogChangeFileDiffAction {
    hash: *hash,
    file: path.clone(),
    action,
    }
    })
    }
    None => el(text("Loading diff..")),
    },
    ])
    .width(Length::FillPortion(1))
    .spacing(SPACING)),
    Some(cursor::Selection::UntrackedFile { .. })
    | Some(cursor::Selection::ChangedFile { .. })
    | Some(cursor::Selection::LogChange { .. })
    | None => el(column([
    repo_info,
    el(column([el(text("Untracked files:")), untracked_files()])),
    el(column([el(text("Changed files:")), changed_files()])),
    el(column([el(text("Recent changes:")), log()])),
    ])
    .width(Length::FillPortion(1))
    .spacing(SPACING)),
    };
    let right_view = el(column([record_msg_editor, selection_details])
    .width(Length::FillPortion(1)));
    el(row([left_view, right_view]).spacing(SPACING))
    } else {
    el(text("Loading repo..."))
    }
    }
    fn view_diff_header(header: String) -> Element<'static, Msg> {
    el(text(header).font(Font {
    weight: font::Weight::Bold,
    ..default()
    }))
    }
    fn selectable_button_style(
    is_selected: bool,
    ) -> impl Fn(&Theme, button::Status) -> button::Style {
    move |theme, status| -> button::Style {
    button::Style {
    border: Border {
    color: if is_selected {
    Color::WHITE
    } else {
    Color::TRANSPARENT
    },
    width: 1.0,
    ..default()
    },
    ..button::Catalog::style(
    theme,
    &<Theme as button::Catalog>::default(),
    status,
    )
    }
    }
    }
    fn display_short_hash(hash: &pijul::Hash) -> String {
    let mut short_hash = pijul::Base32::to_base32(hash);
    short_hash.truncate(8);
    short_hash
    }
  • edit in inflorescence_view/Cargo.toml at line 16
    [52.644]
    [52.644]
    workspace = true
    [dependencies.iced_test]
  • replacement in inflorescence/src/main.rs at line 11
    [32.162][52.700:728]()
    use inflorescence_view::el;
    [32.162]
    [38.365]
    use inflorescence_view::app::RecordMsg;
    use inflorescence_view::{app, theme};
  • replacement in inflorescence/src/main.rs at line 17
    [32.199][24.1316:1394](),[19.1523][24.1316:1394](),[24.1394][27.22:34](),[27.34][53.21:108](),[53.108][27.109:112](),[34.150][27.109:112](),[32.292][27.109:112](),[27.109][27.109:112]()
    use iced::widget::{self, button, column, row, scrollable, text, text_editor};
    use iced::{
    font, window, Border, Color, Element, Font, Length, Subscription, Task,
    Theme,
    };
    [3.1025]
    [16.4438]
    use iced::widget::text_editor;
    use iced::{widget, window, Element, Subscription, Task};
  • edit in inflorescence/src/main.rs at line 23
    [7.351][37.4544:4563]()
    use pijul::Base32;
  • edit in inflorescence/src/main.rs at line 29
    [28.175][31.22:47](),[31.47][24.1453:1454](),[24.1453][24.1453:1454]()
    const SPACING: u32 = 10;
  • replacement in inflorescence/src/main.rs at line 51
    [2.2962][3.1086:1124]()
    fn init() -> (State, Task<Message>) {
    [2.2962]
    [53.163]
    fn init() -> (State, Task<Msg>) {
  • replacement in inflorescence/src/main.rs at line 74
    [14.138][13.5743:5775](),[13.5743][13.5743:5775]()
    Message::RepoTaskExited
    [14.138]
    [13.5775]
    Msg::RepoTaskExited
  • replacement in inflorescence/src/main.rs at line 77
    [13.5848][13.5848:5919]()
    let repo_msg_out_task = Task::run(repo_rx_out, Message::FromRepo);
    [13.5848]
    [8.1996]
    let repo_msg_out_task = Task::run(repo_rx_out, Msg::FromRepo);
  • replacement in inflorescence/src/main.rs at line 81
    [15.639][16.4532:4572]()
    Message::LoadedId(Box::new(id))
    [15.639]
    [15.669]
    Msg::LoadedId(Box::new(id))
  • replacement in inflorescence/src/main.rs at line 87
    [19.2157][53.655:708]()
    open_window_task.map(Message::WindowOpened),
    [19.2157]
    [53.708]
    open_window_task.map(Msg::WindowOpened),
  • replacement in inflorescence/src/main.rs at line 92
    [19.2225][50.105:144]()
    files_task.map(Message::File),
    [19.2225]
    [19.2259]
    files_task.map(Msg::File),
  • edit in inflorescence/src/main.rs at line 97
    [7.1460][53.732:755]()
    window_id,
  • edit in inflorescence/src/main.rs at line 113
    [3.1363][53.756:783]()
    window_id: window::Id,
  • edit in inflorescence/src/main.rs at line 124
    [28.1073][34.1324:1341](),[23.525][17.78:163](),[19.2723][17.78:163](),[17.78][17.78:163](),[2.3039][2.3039:3042]()
    #[derive(Debug)]
    enum RecordMsg {
    Typing(text_editor::Content),
    Canceled { old_msg: String },
    }
  • replacement in inflorescence/src/main.rs at line 125
    [2.3066][5.653:668]()
    enum Message {
    [2.3066]
    [53.784]
    enum Msg {
    View(app::Msg),
  • edit in inflorescence/src/main.rs at line 131
    [13.6224][13.6224:6249](),[13.6249][49.17:42]()
    ToRepo(repo::MsgIn),
    Cursor(cursor::Msg),
  • edit in inflorescence/src/main.rs at line 134
    [11.190][11.190:230](),[11.230][17.164:216]()
    EditRecordMsg(text_editor::Action),
    DeferRecord,
    SaveRecord,
    AbandonRecord,
  • edit in inflorescence/src/main.rs at line 135
    [50.260][34.3034:3064](),[28.2502][34.3034:3064](),[34.3064][50.261:283](),[50.283][35.331:361](),[34.3084][35.331:361](),[35.361][24.1914:1921](),[34.3120][24.1914:1921](),[24.1914][24.1914:1921](),[24.1921][48.18:134]()
    FileDiffsContentsAction {
    id: file::Id,
    action: diff::Action,
    },
    LogChangeFileDiffAction {
    hash: pijul::Hash,
    file: String,
    action: diff::Action,
    },
  • replacement in inflorescence/src/main.rs at line 137
    [2.3099][13.6250:6312]()
    fn update(state: &mut State, msg: Message) -> Task<Message> {
    [2.3099]
    [13.6702]
    fn update(state: &mut State, msg: Msg) -> Task<Msg> {
  • replacement in inflorescence/src/main.rs at line 139
    [13.6718][53.815:866](),[53.866][17.217:252](),[19.3253][17.217:252](),[13.6718][17.217:252]()
    Message::WindowOpened(id) => Task::none(),
    Message::LoadedId(id) => {
    [13.6718]
    [17.252]
    Msg::View(msg) => update_from_app_view(state, msg),
    Msg::WindowOpened(_id) => Task::none(),
    Msg::LoadedId(id) => {
  • replacement in inflorescence/src/main.rs at line 145
    [17.321][13.6718:6755](),[13.6718][13.6718:6755]()
    Message::RepoTaskExited => {
    [17.321]
    [13.6755]
    Msg::RepoTaskExited => {
  • replacement in inflorescence/src/main.rs at line 149
    [13.6830][13.6830:6866]()
    Message::FromRepo(msg) => {
    [13.6830]
    [13.6866]
    Msg::FromRepo(msg) => {
  • edit in inflorescence/src/main.rs at line 152
    [13.6949][13.6949:6993](),[13.6993][16.4597:4646](),[16.4646][12.1273:1298](),[13.7041][12.1273:1298](),[8.3765][12.1273:1298]()
    }
    Message::ToRepo(msg) => {
    state.repo_tx_in.send(msg).unwrap();
    Task::none()
  • replacement in inflorescence/src/main.rs at line 153
    [7.1724][49.43:122](),[49.122][50.284:314](),[50.314][49.158:191](),[49.158][49.158:191](),[49.228][49.228:286](),[49.286][9.6406:6445](),[39.9672][9.6406:6445](),[5.2657][9.6406:6445]()
    Message::Cursor(msg) => cursor::update(
    &mut state.cursor,
    &mut state.files,
    state.repo.as_ref(),
    msg,
    )
    .map(Message::ToRepo),
    Message::AddUntrackedFile => {
    [7.1724]
    [13.11225]
    Msg::AddUntrackedFile => {
  • replacement in inflorescence/src/main.rs at line 191
    [9.7537][10.650:684]()
    Message::RmAddedFile => {
    [9.7537]
    [13.12419]
    Msg::RmAddedFile => {
  • replacement in inflorescence/src/main.rs at line 236
    [11.265][11.265:294]()
    Message::Record => {
    [11.265]
    [13.13952]
    Msg::Record => {
  • replacement in inflorescence/src/main.rs at line 264
    [10.2051][11.604:648]()
    Message::EditRecordMsg(action) => {
    [10.2051]
    [17.1079]
    Msg::File(msg) => {
    file::update(&mut state.files, state.repo.as_ref(), msg);
    Task::none()
    }
    }
    }
    fn update_from_app_view(state: &mut State, msg: app::Msg) -> Task<Msg> {
    match msg {
    app::Msg::Cursor(msg) => cursor::update(
    &mut state.cursor,
    &mut state.files,
    state.repo.as_ref(),
    msg,
    )
    .map(|msg| Msg::View(app::Msg::ToRepo(msg))),
    app::Msg::ToRepo(msg) => {
    state.repo_tx_in.send(msg).unwrap();
    Task::none()
    }
    app::Msg::EditRecordMsg(action) => {
  • replacement in inflorescence/src/main.rs at line 292
    [11.782][17.1193:1226]()
    Message::SaveRecord => {
    [11.782]
    [17.1226]
    app::Msg::SaveRecord => {
  • replacement in inflorescence/src/main.rs at line 320
    [13.14755][17.1384:1418]()
    Message::DeferRecord => {
    [13.14755]
    [17.1418]
    app::Msg::DeferRecord => {
  • replacement in inflorescence/src/main.rs at line 333
    [17.1823][17.1823:1859]()
    Message::AbandonRecord => {
    [17.1823]
    [17.1859]
    app::Msg::AbandonRecord => {
  • replacement in inflorescence/src/main.rs at line 339
    [24.2062][34.13077:13138]()
    Message::FileDiffsContentsAction { id, action } => {
    [24.2062]
    [34.13138]
    app::Msg::FileDiffsContentsAction { id, action } => {
  • replacement in inflorescence/src/main.rs at line 363
    [19.7404][48.135:204]()
    Message::LogChangeFileDiffAction { hash, file, action } => {
    [19.7404]
    [48.204]
    app::Msg::LogChangeFileDiffAction { hash, file, action } => {
  • edit in inflorescence/src/main.rs at line 383
    [48.884][50.511:648]()
    Message::File(msg) => {
    file::update(&mut state.files, state.repo.as_ref(), msg);
    Task::none()
    }
  • replacement in inflorescence/src/main.rs at line 386
    [13.14764][13.14764:14841]()
    fn update_from_repo(state: &mut State, msg: repo::MsgOut) -> Task<Message> {
    [13.14764]
    [13.14841]
    fn update_from_repo(state: &mut State, msg: repo::MsgOut) -> Task<Msg> {
  • replacement in inflorescence/src/main.rs at line 419
    [14.1555][14.1555:1633]()
    Message::ToRepo(repo::MsgIn::RefreshChangedAndUntrackedFiles)
    [14.1555]
    [14.1633]
    Msg::View(app::Msg::ToRepo(
    repo::MsgIn::RefreshChangedAndUntrackedFiles,
    ))
  • replacement in inflorescence/src/main.rs at line 495
    [46.9505][46.9505:9568]()
    let task = Task::done(Message::ToRepo(
    [46.9505]
    [46.9568]
    let task = Task::done(Msg::View(app::Msg::ToRepo(
  • replacement in inflorescence/src/main.rs at line 497
    [46.9634][46.9634:9662]()
    ));
    [46.9634]
    [46.9662]
    )));
  • replacement in inflorescence/src/main.rs at line 571
    [5.2666][5.2666:2717]()
    fn subs(_state: &State) -> Subscription<Message> {
    [24.4299]
    [25.22]
    fn subs(_state: &State) -> Subscription<Msg> {
  • replacement in inflorescence/src/main.rs at line 578
    [5.2903][17.1991:2051](),[17.2051][51.17:292](),[51.292][17.2052:2102](),[49.789][17.2052:2102](),[41.5791][17.2052:2102](),[5.3009][17.2052:2102](),[17.2102][10.2052:2107](),[9.7598][10.2052:2107]()
    "a" => Some(Message::AddUntrackedFile),
    "j" => Some(Message::Cursor(cursor::Msg::Down)),
    "k" => Some(Message::Cursor(cursor::Msg::Up)),
    "h" => Some(Message::Cursor(cursor::Msg::Left)),
    "l" => Some(Message::Cursor(cursor::Msg::Right)),
    "r" => Some(Message::Record),
    "x" => Some(Message::RmAddedFile),
    [5.2903]
    [5.3009]
    "a" => Some(Msg::AddUntrackedFile),
    "j" => Some(Msg::View(app::Msg::Cursor(cursor::Msg::Down))),
    "k" => Some(Msg::View(app::Msg::Cursor(cursor::Msg::Up))),
    "h" => Some(Msg::View(app::Msg::Cursor(cursor::Msg::Left))),
    "l" => {
    Some(Msg::View(app::Msg::Cursor(cursor::Msg::Right)))
    }
    "r" => Some(Msg::Record),
    "x" => Some(Msg::RmAddedFile),
  • replacement in inflorescence/src/main.rs at line 590
    [49.845][51.293:354]()
    Some(Message::Cursor(cursor::Msg::Down))
    [49.845]
    [49.912]
    Some(Msg::View(app::Msg::Cursor(cursor::Msg::Down)))
  • replacement in inflorescence/src/main.rs at line 593
    [49.983][51.355:414]()
    Some(Message::Cursor(cursor::Msg::Up))
    [49.983]
    [49.1048]
    Some(Msg::View(app::Msg::Cursor(cursor::Msg::Up)))
  • replacement in inflorescence/src/main.rs at line 596
    [49.1121][51.415:476]()
    Some(Message::Cursor(cursor::Msg::Left))
    [49.1121]
    [49.1188]
    Some(Msg::View(app::Msg::Cursor(cursor::Msg::Left)))
  • replacement in inflorescence/src/main.rs at line 599
    [41.5928][51.477:539]()
    Some(Message::Cursor(cursor::Msg::Right))
    [41.5928]
    [41.5975]
    Some(Msg::View(app::Msg::Cursor(cursor::Msg::Right)))
  • replacement in inflorescence/src/main.rs at line 607
    [17.2159][17.2159:2212]()
    Some(Message::AbandonRecord)
    [17.2159]
    [17.2212]
    Some(Msg::View(app::Msg::AbandonRecord))
  • replacement in inflorescence/src/main.rs at line 610
    [17.2290][17.2290:2341]()
    Some(Message::DeferRecord)
    [17.2290]
    [17.2341]
    Some(Msg::View(app::Msg::DeferRecord))
    }
    "s" if mods == Modifiers::CTRL => {
    Some(Msg::View(app::Msg::SaveRecord))
  • edit in inflorescence/src/main.rs at line 615
    [17.2363][17.2363:2444]()
    "s" if mods == Modifiers::CTRL => Some(Message::SaveRecord),
  • edit in inflorescence/src/main.rs at line 621
    [5.3183][2.3221:3224](),[4.5139][2.3221:3224](),[19.8474][2.3221:3224](),[2.3221][2.3221:3224](),[2.3224][53.867:927](),[53.927][45.16960:16982](),[45.16960][45.16960:16982](),[30.3294][29.6096:6099](),[34.16535][29.6096:6099](),[45.16982][29.6096:6099](),[29.6096][29.6096:6099](),[29.6099][53.928:997](),[53.997][13.16477:16523](),[3.1501][13.16477:16523](),[13.16523][17.2445:2560](),[17.2560][13.16682:16833](),[13.16682][13.16682:16833](),[13.16833][5.3530:3531](),[4.5403][5.3530:3531](),[5.3531][45.16983:17085](),[17.2624][8.7755:7859](),[13.16938][8.7755:7859](),[45.17085][8.7755:7859](),[8.7755][8.7755:7859](),[8.7859][39.10971:11075](),[39.11075][8.7965:7984](),[34.16652][8.7965:7984](),[8.7965][8.7965:7984](),[8.7984][17.2625:2645](),[17.2645][8.8015:8054](),[8.8015][8.8015:8054](),[8.8054][51.540:611](),[51.611][34.16653:16736](),[49.1353][34.16653:16736](),[8.8111][34.16653:16736](),[34.16736][49.1354:1382](),[49.1382][8.8224:8327](),[8.8224][8.8224:8327](),[8.8327][45.17086:17109](),[45.17109][8.8340:8341](),[8.8340][8.8340:8341](),[8.8341][45.17110:17208](),[45.17208][22.4521:4563](),[17.2707][22.4521:4563](),[22.4563][39.11076:11254](),[6.1118][5.3822:3841](),[8.8446][5.3822:3841](),[39.11254][5.3822:3841](),[34.16850][5.3822:3841](),[5.3822][5.3822:3841](),[5.3841][17.2708:2728](),[17.2728][18.1047:1075](),[18.1075][26.298:331](),[26.331][51.612:683](),[51.683][34.16851:16937](),[49.1460][34.16851:16937](),[5.3968][34.16851:16937](),[34.16937][49.1461:1489](),[49.1489][5.4052:4155](),[5.4052][5.4052:4155](),[5.4155][45.17209:17232](),[45.17232][5.4168:4169](),[5.4168][5.4168:4169](),[5.4169][45.17233:17311](),[45.17311][39.11316:11451](),[39.11316][39.11316:11451](),[39.11451][45.17312:17371](),[45.17371][39.11452:11648](),[37.5173][39.11452:11648](),[39.11648][37.5238:5333](),[37.5238][37.5238:5333](),[37.5333][51.684:755](),[51.755][45.17372:17473](),[49.1567][45.17372:17473](),[37.5390][45.17372:17473](),[45.17473][49.1568:1596](),[49.1596][37.5485:5664](),[37.5485][37.5485:5664](),[37.5664][45.17474:17497](),[17.2825][13.17203:17204](),[37.5677][13.17203:17204](),[45.17497][13.17203:17204](),[13.17203][13.17203:17204](),[13.17204][17.2826:3500](),[17.3500][19.8475:8559](),[19.8559][47.1782:1854](),[47.1854][50.1239:1275](),[50.1275][34.17129:17169](),[34.17129][34.17129:17169](),[34.17169][50.1276:1330](),[50.1330][34.17221:17240](),[34.17221][34.17221:17240](),[34.17240][50.1331:1463](),[50.1463][47.1855:1929](),[34.17364][47.1855:1929](),[47.1929][35.1245:1383](),[34.17364][35.1245:1383](),[35.1383][34.17510:17603](),[34.17510][34.17510:17603](),[34.17603][35.1384:1441](),[35.1441][34.17660:17682](),[34.17660][34.17660:17682](),[34.17682][50.1464:1522](),[50.1522][45.17554:17628](),[45.17554][45.17554:17628](),[28.6889][19.9266:9285](),[45.17628][19.9266:9285](),[34.17760][19.9266:9285](),[19.9266][19.9266:9285](),[19.9285][34.17761:17789](),[34.17789][45.17629:17757](),[45.17757][34.17852:17895](),[34.17852][34.17852:17895](),[34.17895][45.17758:17812](),[27.380][19.9339:9353](),[20.1352][19.9339:9353](),[45.17812][19.9339:9353](),[34.17915][19.9339:9353](),[19.9339][19.9339:9353](),[19.9353][47.1930:2000](),[47.2000][50.1523:1559](),[50.1559][34.18105:18145](),[34.18105][34.18105:18145](),[34.18145][50.1560:1612](),[50.1612][34.18195:18214](),[34.18195][34.18195:18214](),[34.18214][50.1613:1745](),[50.1745][47.2001:2075](),[34.18338][47.2001:2075](),[47.2075][35.1442:1580](),[34.18338][35.1442:1580](),[35.1580][34.18484:18577](),[34.18484][34.18484:18577](),[34.18577][35.1581:1638](),[35.1638][33.4823:4845](),[34.18604][33.4823:4845](),[33.4823][33.4823:4845](),[33.4845][50.1746:1804](),[50.1804][45.17869:17943](),[45.17869][45.17869:17943](),[45.17943][34.18683:18730](),[34.18683][34.18683:18730](),[34.18730][45.17944:18020](),[45.18020][34.18793:18836](),[34.18793][34.18793:18836](),[34.18836][45.18021:18075](),[45.18075][37.5678:5692](),[34.18856][37.5678:5692](),[37.5692][45.18076:18191](),[45.18191][46.11660:11686](),[46.11686][45.18191:18313](),[45.18191][45.18191:18313](),[45.18313][39.11827:11828](),[39.11827][39.11827:11828](),[39.11828][45.18314:18373](),[45.18373][39.11923:11924](),[39.11923][39.11923:11924](),[39.11924][41.5994:6077](),[41.6077][45.18374:18518](),[45.18518][41.6078:6144](),[39.12134][41.6078:6144](),[41.6144][51.756:875](),[51.875][39.12270:12386](),[49.1722][39.12270:12386](),[41.6248][39.12270:12386](),[39.12270][39.12270:12386](),[39.12386][45.18519:18698](),[45.18698][39.12466:12517](),[39.12466][39.12466:12517](),[39.12517][45.18699:18753](),[21.515][19.9691:9738](),[33.4970][19.9691:9738](),[37.5774][19.9691:9738](),[39.12537][19.9691:9738](),[45.18753][19.9691:9738](),[34.18856][19.9691:9738](),[19.9691][19.9691:9738](),[19.9738][17.3500:3511](),[17.3500][17.3500:3511](),[17.3511][5.4170:4171](),[13.17593][5.4170:4171](),[4.5731][5.4170:4171](),[5.4171][45.18754:18939](),[45.18939][46.11687:11786](),[46.11786][45.19021:19203](),[45.19021][45.19021:19203](),[45.19203][46.11787:11924](),[46.11924][48.885:1211](),[48.1211][45.19347:19740](),[47.2152][45.19347:19740](),[45.19347][45.19347:19740](),[45.19740][17.3553:3580](),[17.3553][17.3553:3580](),[17.3580][45.19741:19961](),[37.5839][17.3863:3878](),[45.19961][17.3863:3878](),[17.3863][17.3863:3878](),[17.3878][24.5482:5557](),[24.5557][45.19962:20153](),[24.5596][11.1940:1953](),[13.18286][11.1940:1953](),[45.20153][11.1940:1953](),[11.1940][11.1940:1953](),[11.1953][17.3999:4035](),[17.4035][13.18334:18340](),[13.18334][13.18334:18340](),[13.18340][27.445:611]()
    }
    fn theme(_state: &State, _window_id: window::Id) -> Theme {
    Theme::TokyoNight
    }
    fn view(state: &State, _window_id: window::Id) -> Element<Message> {
    if let Some(repo) = state.repo.as_ref() {
    let repo_info = el(row([
    el(text(&repo.dir_name)),
    el(text(": ")),
    el(
    button(text(&repo.channel)), /* TODO
    * .on_press(Message) */
    ),
    ]));
    let untracked_files = || {
    el(column(repo.untracked_files.iter().enumerate().map(
    |(ix, path)| {
    let is_selected = matches!(state.cursor.selection.as_ref() ,
    Some(cursor::Selection::UntrackedFile{ ix: selected_ix, .. }) if &ix == selected_ix
    );
    el(
    button(text(path))
    .on_press(Message::Cursor(cursor::Msg::Select(
    cursor::Select::UntrackedFile{ix, path: path.clone()},
    )))
    .style(selectable_button_style(is_selected)),
    )
    },
    )))
    };
    let changed_files = || {
    el(column(repo.changed_files.iter().enumerate().map(
    |(ix, (file_path, _diffs))| {
    let is_selected = matches!(state.cursor.selection.as_ref(),
    Some(cursor::Selection::ChangedFile{ ix: selected_ix, .. }) if &ix == selected_ix
    );
    el(
    button(
    text(file_path))
    .on_press(Message::Cursor(cursor::Msg::Select(
    cursor::Select::ChangedFile{ix, path: file_path.clone()},
    )))
    .style(selectable_button_style(is_selected)),
    )
    },
    )))
    };
    let log = || {
    el(column(repo.log.iter().enumerate().map(
    |(ix, repo::LogEntry {
    hash,
    message,
    file_paths: _,
    })| {
    let short_hash = display_short_hash(hash);
    let is_selected = matches!(state.cursor.selection.as_ref(),
    Some(cursor::Selection::LogChange { ix: selected_ix, .. }) if &ix == selected_ix
    );
    el(row([
    el(button(text(short_hash).font(Font::MONOSPACE))
    .on_press(Message::Cursor(cursor::Msg::Select(
    cursor::Select::LogChange { ix, hash: *hash, message: message.clone() },
    )))
    .style(selectable_button_style(is_selected))),
    el(text(message)),
    ])
    .spacing(SPACING))
    },
    )))
    };
    let record_msg_editor = if let Some(RecordMsg::Typing(msg_content)) =
    state.record_msg.as_ref()
    {
    el(column([
    el(text_editor(msg_content)
    .placeholder("Type something here...")
    .on_action(Message::EditRecordMsg)),
    el(row([
    el(button(text("Save")).on_press(Message::SaveRecord)),
    el(button(text("Defer")).on_press(Message::DeferRecord)),
    el(button(text("Abandon"))
    .on_press(Message::AbandonRecord)),
    ])),
    ]))
    } else {
    el(row([]))
    };
    let selection_details = match state.cursor.selection.as_ref() {
    Some(cursor::Selection::UntrackedFile { ix: _, path }) => {
    let id = file::Id {
    path: path.clone(),
    file_kind: file::Kind::Untracked,
    };
    let diffs = match state.files.diffs_cache.inner.peek(&id) {
    Some(file::Diff::Loaded(file)) => {
    let selection_state = state.diffs_state.get(&id);
    diff::view(selection_state, file).map(move |msg| {
    Message::FileDiffsContentsAction {
    id: id.clone(),
    action: msg,
    }
    })
    }
    None | Some(file::Diff::Loading) => {
    el(text("Loading diff..."))
    }
    };
    el(column([
    view_diff_header(format!(
    "Untracked file {path} contents:"
    )),
    el(scrollable(diffs)),
    ])
    .spacing(SPACING))
    }
    Some(cursor::Selection::ChangedFile { path, ix: _ }) => {
    let id = file::Id {
    path: path.clone(),
    file_kind: file::Kind::Changed,
    };
    let diffs = match state.files.diffs_cache.inner.peek(&id) {
    Some(file::Diff::Loaded(file)) => {
    let selection_state = state.diffs_state.get(&id);
    diff::view(selection_state, file).map(move |msg| {
    Message::FileDiffsContentsAction {
    id: id.clone(),
    action: msg,
    }
    })
    }
    None | Some(file::Diff::Loading) => {
    el(text("Loading diff..."))
    }
    };
    el(column([
    view_diff_header(format!("Changed file {path} diff:")),
    el(scrollable(diffs)),
    ])
    .spacing(SPACING))
    }
    Some(cursor::Selection::LogChange {
    ix,
    hash,
    message,
    diffs: _,
    file,
    }) => {
    let entry = state.repo.as_ref().unwrap().log.get(*ix).unwrap();
    let short_hash = display_short_hash(hash);
    let files = entry.file_paths.iter().enumerate().map(|(ix, path)| {
    let is_selected = matches!(file, Some(cursor::LogChangeFileSelection{ path: selected_path, .. }) if selected_path == path);
    el(button(text(path)).on_press_with(move || {
    Message::Cursor(cursor::Msg::Select(cursor::Select::LogChangeFile { ix, path: path.clone() }))
    }).style(selectable_button_style(is_selected)))
    });
    el(column([
    view_diff_header(format!("{short_hash} message:")),
    el(text(message)),
    view_diff_header("Changed files:".to_string()),
    el(scrollable(column(files))),
    ])
    .spacing(SPACING))
    }
    None => el(row([])),
    };
    let left_view = match state.cursor.selection.as_ref() {
    Some(cursor::Selection::LogChange {
    ix: _,
    hash,
    message: _,
    diffs,
    file: Some(cursor::LogChangeFileSelection { ix: _, path }),
    }) => el(column([
    view_diff_header(format!(
    "{path} changes in {}:",
    display_short_hash(hash)
    )),
    match diffs {
    Some(diffs) => {
    let (file, state) = diffs.get(path).unwrap();
    diff::view(Some(state), file).map(|action| {
    Message::LogChangeFileDiffAction {
    hash: *hash,
    file: path.clone(),
    action,
    }
    })
    }
    None => el(text("Loading diff..")),
    },
    ])
    .width(Length::FillPortion(1))
    .spacing(SPACING)),
    Some(cursor::Selection::UntrackedFile { .. })
    | Some(cursor::Selection::ChangedFile { .. })
    | Some(cursor::Selection::LogChange { .. })
    | None => el(column([
    repo_info,
    el(column([el(text("Untracked files:")), untracked_files()])),
    el(column([el(text("Changed files:")), changed_files()])),
    el(column([el(text("Recent changes:")), log()])),
    ])
    .width(Length::FillPortion(1))
    .spacing(SPACING)),
    };
    let right_view = el(column([record_msg_editor, selection_details])
    .width(Length::FillPortion(1)));
    el(row([left_view, right_view]).spacing(SPACING))
    } else {
    el(text("Loading repo..."))
    }
    }
    fn view_diff_header(header: String) -> Element<'static, Message> {
    el(text(header).font(Font {
    weight: font::Weight::Bold,
    ..default()
    }))
  • replacement in inflorescence/src/main.rs at line 623
    [24.6306][5.4175:4777](),[5.4175][5.4175:4777]()
    fn selectable_button_style(
    is_selected: bool,
    ) -> impl Fn(&Theme, button::Status) -> button::Style {
    move |theme, status| -> button::Style {
    button::Style {
    border: Border {
    color: if is_selected {
    Color::WHITE
    } else {
    Color::TRANSPARENT
    },
    width: 1.0,
    ..default()
    },
    ..button::Catalog::style(
    theme,
    &<Theme as button::Catalog>::default(),
    status,
    )
    }
    }
    [5.4175]
    [45.20154]
    fn view(state: &State, window_id: window::Id) -> Element<Msg> {
    let State {
    id: _,
    repo_fs_watch: _,
    repo_path,
    repo_tx_in: _,
    repo,
    cursor,
    record_msg,
    files,
    diffs_state,
    } = state;
    app::view(
    app::State {
    repo_path,
    repo: repo.as_ref(),
    cursor,
    record_msg: record_msg.as_ref(),
    diffs_state,
    },
    window_id,
    |id| files.diffs_cache.inner.peek(id),
    )
    .map(Msg::View)
  • edit in inflorescence/src/main.rs at line 648
    [45.20156][45.20156:20297](),[3.1579][2.3333:3335](),[17.4129][2.3333:3335](),[5.4777][2.3333:3335](),[4.6123][2.3333:3335](),[45.20297][2.3333:3335](),[2.3333][2.3333:3335]()
    fn display_short_hash(hash: &pijul::Hash) -> String {
    let mut short_hash = hash.to_base32();
    short_hash.truncate(8);
    short_hash
    }
  • edit in inflorescence/src/file.rs at line 2
    [50.1869]
    [50.1869]
    pub use inflorescence_view::app::file::{Diff, Id, Kind};
  • edit in inflorescence/src/file.rs at line 49
    [50.2898][50.2898:3180]()
    #[derive(Debug)]
    pub enum Diff {
    Loading,
    Loaded(diff::File),
    }
    #[derive(Debug, Clone, Hash, PartialEq, Eq)]
    pub struct Id {
    pub path: String,
    pub file_kind: Kind,
    }
    #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
    pub enum Kind {
    Untracked,
    Changed,
    }
  • replacement in inflorescence/src/diff.rs at line 3
    [33.5056][34.19159:19181](),[34.19181][33.5056:5070](),[33.5056][33.5056:5070]()
    use std::borrow::Cow;
    use std::cmp;
    [33.5056]
    [52.731]
    pub use inflorescence_view::diff::*;
  • edit in inflorescence/src/diff.rs at line 5
    [52.732][52.732:760](),[52.760][33.5070:5071](),[33.5070][33.5070:5071](),[33.5071][42.19:69](),[42.69][45.20299:20364]()
    use inflorescence_view::el;
    use iced::widget::{column, container, row, text};
    use iced::{alignment, Background, Color, Element, Font, Length};
  • replacement in inflorescence/src/diff.rs at line 7
    [33.5154][42.124:287]()
    // TODO: maybe use theme
    const DELETED_BG_COLOR: Color = Color::from_rgba8(190, 37, 40, 0.15);
    const ADDED_BG_COLOR: Color = Color::from_rgba8(47, 148, 11, 0.15);
    [33.5154]
    [42.287]
    use std::borrow::Cow;
    use std::cmp;
  • replacement in inflorescence/src/diff.rs at line 10
    [42.288][35.1744:1927](),[35.1744][35.1744:1927]()
    #[derive(Debug, Default)]
    pub struct State {
    pub selected_sections: Vec<usize>,
    pub expanded_unchanged_sections: Vec<usize>,
    pub collapsed_changed_sections: Vec<usize>,
    }
    [42.288]
    [35.1927]
    pub fn update(_state: &mut State, _msg: Msg) {}
  • edit in inflorescence/src/diff.rs at line 12
    [35.1928][35.1928:2027](),[35.2027][47.2154:2248](),[47.2248][35.2102:2395](),[35.2102][35.2102:2395](),[35.2395][36.23:121](),[36.121][33.5154:5171](),[35.2489][33.5154:5171](),[33.5154][33.5154:5171](),[33.5171][34.19212:19291]()
    #[derive(Debug, Clone)]
    pub enum Action {}
    pub fn update(_state: &mut State, _action: Action) {}
    pub fn view<'a>(
    state: Option<&'a State>,
    file: &'a File,
    ) -> Element<'a, Action> {
    match file {
    File::Decoded(decoded_file) => view_decoded(state, decoded_file),
    File::Undecodable(undecodable_file) => {
    view_undecodable(state, undecodable_file)
    }
    }
    }
    /// [`File`] is not part of [`State`] so it can be stored separately (i.e. in a
    /// cache, where it's immutable once set, unlike [`State`] which can change with
    /// [`Action`]s)
    #[derive(Debug)]
    pub enum File {
    Decoded(DecodedFile),
    Undecodable(UndecodableFile),
    }
  • edit in inflorescence/src/diff.rs at line 13
    [34.19308][34.19308:19419](),[34.19419][33.5517:5537](),[33.5517][33.5517:5537](),[33.5537][34.19420:19559](),[34.19559][33.5767:5770](),[33.5767][33.5767:5770](),[33.5770][34.19560:19609](),[34.19609][33.5770:5787](),[33.5770][33.5770:5787](),[33.5787][34.19610:19632](),[34.19632][33.5806:5887](),[33.5806][33.5806:5887]()
    pub struct DecodedFile {
    pub combined: Combined,
    pub diffs_without_contents: Vec<DiffWithoutContents>,
    }
    #[derive(Debug)]
    pub struct UndecodableFile {
    pub diffs_with_contents: Vec<DiffWithContents>,
    pub diffs_without_contents: Vec<DiffWithoutContents>,
    }
    /// A file combined with its diffs into sections
    #[derive(Debug)]
    pub struct Combined {
    pub sections: Vec<Section>,
    pub max_line_num: usize,
    }
    #[derive(Debug)]
  • edit in inflorescence/src/diff.rs at line 16
    [35.2511][35.2511:2842]()
    }
    #[derive(Debug)]
    pub enum DiffWithContents {
    Add,
    Edit {
    line: usize,
    deleted: bool,
    contents: String,
    },
    Replacement {
    line: usize,
    /// Deleted line
    change_contents: String,
    /// Added lines
    replacement_contents: String,
    },
    Del,
    Undel,
  • edit in inflorescence/src/diff.rs at line 18
    [35.2915][35.2915:3717](),[35.3787][35.3787:4010](),[35.4010][34.19687:19708](),[34.19687][34.19687:19708](),[34.19708][35.4011:4288](),[35.4288][34.19708:19710](),[34.19708][34.19708:19710](),[34.19710][35.4289:4431](),[35.4431][34.19710:19711](),[34.19710][34.19710:19711]()
    #[derive(Debug)]
    pub enum DiffWithoutContents {
    // _________________________________________________________________________
    // Cases that never have contents:
    Move,
    SolveNameConflict,
    UnsolveNameConflict,
    SolveOrderConflict,
    UnsolveOrderConflict,
    ResurrectZombines,
    AddRoot,
    DelRoot,
    // _________________________________________________________________________
    // Cases that normally have contents, but in these cases the contents are
    // not decodable:
    Edit {
    line: usize,
    deleted: bool,
    contents: UndecodableContents,
    },
    Replacement {
    line: usize,
    /// Deleted line
    change_contents: UndecodableContents,
    /// Added lines
    replacement_contents: UndecodableContents,
    },
    }
    #[derive(Debug)]
    pub enum UndecodableContents {
    /// Short byte sequence of unknown encoding encoded with base64 for
    /// display. Must be shorter than [`crate::repo::MAX_LEN_BASE64_DISPLAY`]
    ShortBase64(String),
    UnknownEncoding,
    }
    #[derive(Debug)]
    pub enum Section {
    Unchanged(Lines),
    /// `deleted` and `added` are together because for
    /// `ChangedFileDiffWithContents::Replacement` they begin on the same line
    /// number
    Changed {
    deleted: Lines,
    added: Lines,
    },
    }
    /// INVARIANT: There must be no new-lines in any of the strings, the source
    /// string must be split on those.
    pub type Lines = Vec<String>;
  • edit in inflorescence/src/diff.rs at line 247
    [33.9933][33.9933:9939](),[34.25625][34.25625:25628](),[34.27679][34.27679:27782](),[34.27782][43.19:242]()
    }
    }
    fn contents_to_lines(contents: &str) -> Lines {
    contents.split('\n').map(str::to_string).collect()
    }
    fn undecodable_contents_to_str(contents: &UndecodableContents) -> &str {
    match contents {
    UndecodableContents::ShortBase64(short) => short,
    UndecodableContents::UnknownEncoding => "Unknown encoding",
  • edit in inflorescence/src/diff.rs at line 263
    [34.28144][33.10832:10833](),[33.10832][33.10832:10833](),[33.10833][35.8703:8724](),[35.8724][47.2249:2280](),[47.2280][34.28193:28220](),[34.28193][34.28193:28220](),[34.28220][35.8725:8752](),[35.8752][42.289:444](),[42.444][34.28244:28318](),[35.8752][34.28244:28318](),[34.28244][34.28244:28318](),[34.28318][33.10886:10916](),[33.10886][33.10886:10916](),[33.10916][44.163:556](),[44.556][33.11717:11718](),[33.11717][33.11717:11718](),[33.11718][44.557:1339](),[44.1339][33.12921:12922](),[33.12921][33.12921:12922](),[33.12922][44.1340:1427](),[44.1427][42.1849:2223](),[33.13042][42.1849:2223](),[42.2223][34.28439:28442](),[33.13072][34.28439:28442](),[34.28442][35.8753:8778](),[35.8778][47.2281:2312](),[47.2312][34.28494:28525](),[34.28494][34.28494:28525](),[34.28525][35.8779:8806](),[35.8806][42.2224:2553](),[42.2553][45.20365:20399](),[45.20399][42.2575:3093](),[42.2575][42.2575:3093](),[42.3093][44.1428:1817](),[44.1817][42.3422:3977](),[42.3422][42.3422:3977](),[42.4025][42.4025:4067](),[42.4067][44.1818:1847](),[44.1847][42.4091:4156](),[42.4091][42.4091:4156](),[42.4156][44.1848:2054](),[44.2054][42.4611:4630](),[42.4611][42.4611:4630](),[42.4630][44.2055:2125](),[44.2125][42.4695:4730](),[42.4695][42.4695:4730](),[42.4730][44.2126:2354](),[44.2354][42.5223:5427](),[42.5223][42.5223:5427](),[42.5427][33.13072:13075](),[34.28618][33.13072:13075](),[33.13072][33.13072:13075](),[33.13075][42.5428:5586](),[42.5586][43.249:948](),[43.948][42.6031:6128](),[42.6031][42.6031:6128](),[42.6128][43.949:1061](),[43.1061][44.2355:2607](),[44.2607][43.1270:1280](),[43.1270][43.1270:1280](),[43.1280][42.6150:6274](),[42.6150][42.6150:6274](),[42.6274][43.1281:1533](),[43.1533][44.2608:2755](),[44.2755][43.1952:1978](),[43.1952][43.1952:1978](),[43.1978][42.6296:6305](),[42.6296][42.6296:6305](),[42.6305][44.2756:2835](),[44.2835][45.20400:20535](),[45.20535][33.13185:13187](),[33.13185][33.13185:13187](),[33.13187][42.6385:6509](),[42.6509][45.20536:20880](),[45.20880][42.6789:6791](),[42.6789][42.6789:6791](),[42.6791][44.2899:3777]()
    fn view_decoded<'a>(
    _state: Option<&'a State>,
    file: &'a DecodedFile,
    ) -> Element<'a, Action> {
    let DecodedFile {
    combined,
    diffs_without_contents,
    } = file;
    let line_num_digits = combined.max_line_num.to_string().len();
    // TODO use state to display selection, and control section expansion
    let mut current_line = 1;
    let sections_view = combined.sections.iter().map(|section| match section {
    Section::Unchanged(lines) => {
    let res = lines.iter().enumerate().map(move |(ix, line)| {
    line_view(
    LineKind::Unchanged,
    current_line + ix,
    line_num_digits,
    line,
    )
    });
    current_line += lines.len();
    el(column(res))
    }
    Section::Changed { deleted, added } => {
    let res = deleted
    .iter()
    .enumerate()
    .map(move |(ix, line)| {
    line_view(
    LineKind::Deleted,
    current_line + ix,
    line_num_digits,
    line,
    )
    })
    .chain(added.iter().enumerate().map(move |(ix, line)| {
    line_view(
    LineKind::Added,
    current_line + ix,
    line_num_digits,
    line,
    )
    }));
    current_line += added.len();
    el(column(res))
    }
    });
    if diffs_without_contents.is_empty() {
    el(column(sections_view))
    } else {
    let diffs_without_contents_view = diffs_without_contents
    .iter()
    .map(view_diff_without_contents);
    el(column([
    el(column(diffs_without_contents_view)),
    el(column(sections_view)),
    ])
    .spacing(10))
    }
    }
    fn view_undecodable<'a>(
    _state: Option<&'a State>,
    file: &'a UndecodableFile,
    ) -> Element<'a, Action> {
    let UndecodableFile {
    diffs_with_contents,
    diffs_without_contents,
    } = file;
    let diffs = diffs_with_contents
    .iter()
    .map(view_diff_with_contents)
    .chain(
    diffs_without_contents
    .iter()
    .map(view_diff_without_contents),
    );
    el(column(diffs).spacing(10))
    }
    /// View diffs without context (the file contents)
    fn view_diff_with_contents(diff: &DiffWithContents) -> Element<'_, Action> {
    match diff {
    DiffWithContents::Add => el(text("Added")),
    DiffWithContents::Edit {
    line,
    deleted,
    contents,
    } => {
    let line_num = *line;
    let lines = contents_to_lines(contents);
    let max_line_num = line_num + lines.len();
    let line_num_digits = max_line_num.to_string().len();
    let lines_view = lines.into_iter().enumerate().map(|(ix, line)| {
    line_view(
    if *deleted {
    LineKind::Deleted
    } else {
    LineKind::Added
    },
    line_num + ix,
    line_num_digits,
    line,
    )
    });
    el(column(lines_view))
    }
    DiffWithContents::Replacement {
    line,
    change_contents,
    replacement_contents,
    } => {
    let line_num = *line;
    let change_lines = contents_to_lines(change_contents);
    let replacement_lines = contents_to_lines(replacement_contents);
    let max_line_num = line_num
    + cmp::max(change_lines.len(), replacement_lines.len());
    let line_num_digits = max_line_num.to_string().len();
    let lines_view = change_lines
    .into_iter()
    .enumerate()
    .map(|(ix, line)| {
    line_view(
    LineKind::Deleted,
    line_num + ix,
    line_num_digits,
    line,
    )
    })
    .chain(replacement_lines.into_iter().enumerate().map(
    |(ix, line)| {
    line_view(
    LineKind::Added,
    line_num + ix,
    line_num_digits,
    line,
    )
    },
    ));
    el(column(lines_view))
    }
    DiffWithContents::Del => el(text("Deleted")),
    DiffWithContents::Undel => el(text("Revived")),
    }
    }
    /// View diffs without context (the file contents)
    fn view_diff_without_contents(
    diff: &DiffWithoutContents,
    ) -> Element<'_, Action> {
    match diff {
    DiffWithoutContents::Move => el(text("Move")),
    DiffWithoutContents::SolveNameConflict => {
    el(text("Solve name conflict"))
    }
    DiffWithoutContents::UnsolveNameConflict => {
    el(text("Unsolve name conflict"))
    }
    DiffWithoutContents::SolveOrderConflict => {
    el(text("Solve order conflict"))
    }
    DiffWithoutContents::UnsolveOrderConflict => {
    el(text("Unsolve order conflict"))
    }
    DiffWithoutContents::ResurrectZombines => el(text("Resurrect zombies")),
    DiffWithoutContents::AddRoot => el(text("Add root")),
    DiffWithoutContents::DelRoot => el(text("Delete root")),
    DiffWithoutContents::Edit {
    line,
    deleted,
    contents,
    } => {
    let line_num = *line;
    let line = undecodable_contents_to_str(contents);
    line_view(
    if *deleted {
    LineKind::Deleted
    } else {
    LineKind::Added
    },
    line_num,
    1,
    line,
    )
    }
    DiffWithoutContents::Replacement {
    line,
    change_contents,
    replacement_contents,
    } => {
    let line_num = *line;
    let change_line = undecodable_contents_to_str(change_contents);
    let replacement_line =
    undecodable_contents_to_str(replacement_contents);
    el(column([
    line_view(LineKind::Deleted, line_num, 1, change_line),
    line_view(LineKind::Added, line_num, 1, replacement_line),
    ]))
    }
    }
    }
    fn mono_text<'a>(txt: impl text::IntoFragment<'a>) -> iced::widget::Text<'a> {
    text(txt)
    .font(Font::MONOSPACE)
    .wrapping(text::Wrapping::WordOrGlyph)
    .align_y(alignment::Vertical::Top)
    }
    fn line_num_view<'a>(num: usize, digits: usize) -> iced::widget::Text<'a> {
    // Fill the string to the number of digits
    let txt = format!("{num:digits$} ");
    mono_text(txt)
    .font(Font::MONOSPACE)
    .style(move |theme| {
    let palette = theme.extended_palette();
    text::Style {
    color: Some(palette.background.base.text.scale_alpha(0.61)),
    }
    })
    .align_y(alignment::Vertical::Top)
    }
    #[derive(Debug, Clone, Copy)]
    enum LineKind {
    Unchanged,
    Added,
    Deleted,
    }
    fn line_view<'a>(
    kind: LineKind,
    line_num: usize,
    line_num_digits: usize,
    line: impl text::IntoFragment<'a>,
    ) -> Element<'a, Action> {
    let line = container(row([
    el(mono_text(match kind {
    LineKind::Unchanged => " ",
    LineKind::Added => "+ ",
    LineKind::Deleted => "- ",
    })),
    el(line_num_view(line_num, line_num_digits)),
    el(mono_text(line).width(Length::Fill)),
    ]));
    el(match kind {
    LineKind::Unchanged => line,
    LineKind::Added => line.style(|_theme| {
    container::background(Background::from(ADDED_BG_COLOR))
    }),
    LineKind::Deleted => line.style(|_theme| {
    container::background(Background::from(DELETED_BG_COLOR))
    }),
    })
    }
  • edit in inflorescence/src/cursor.rs at line 1
    [5.26]
    [50.8494]
    pub use inflorescence_view::app::cursor::{
    LogChangeFileSelection, Msg, Select, Selection, State,
    };
  • edit in inflorescence/src/cursor.rs at line 6
    [50.8519][50.8519:8551]()
    use libflorescence::prelude::*;
  • edit in inflorescence/src/cursor.rs at line 8
    [50.8578][46.11927:11959](),[5.26][46.11927:11959]()
    use std::collections::HashMap;
  • edit in inflorescence/src/cursor.rs at line 9
    [49.2147][34.18884:18885](),[35.8825][34.18884:18885](),[34.18884][34.18884:18885](),[34.18885][5.27:113](),[5.26][5.27:113](),[5.113][49.2148:2187](),[49.2187][51.879:938](),[51.938][49.2276:2279](),[49.2276][49.2276:2279](),[49.2279][34.18886:18903](),[5.113][34.18886:18903](),[34.18903][5.137:158](),[5.137][5.137:158](),[5.158][34.18904:18965](),[35.8854][34.19002:19068](),[34.19002][34.19002:19068](),[35.8883][37.5872:5879](),[37.5879][39.12540:12575](),[39.12575][37.5892:5919](),[37.5892][37.5892:5919](),[37.5919][45.20882:20907](),[45.20907][46.11960:12251](),[46.12251][39.12576:12622](),[45.20907][39.12576:12622](),[37.5919][39.12576:12622](),[37.5919][34.19105:19112](),[35.8883][34.19105:19112](),[39.12622][34.19105:19112](),[34.19105][34.19105:19112](),[34.19112][39.12623:12720](),[39.12720][34.19112:19114](),[45.20978][34.19112:19114](),[34.19112][34.19112:19114]()
    #[derive(Debug, Default)]
    pub struct State {
    pub selection: Option<Selection>,
    }
    #[derive(Debug, Clone)]
    pub enum Msg {
    Down,
    Up,
    Right,
    Left,
    Select(Select),
    }
    #[derive(Debug)]
    pub enum Selection {
    UntrackedFile {
    ix: usize,
    path: String,
    },
    ChangedFile {
    ix: usize,
    path: String,
    },
    LogChange {
    ix: usize,
    hash: pijul::Hash,
    message: String,
    /// All the diffs in this change keyed by file path. Loaded async and
    /// set to None only while loading. The `diff::State` is also
    /// in here so that is it preserved while navigating between files.
    diffs: Option<HashMap<String, (diff::File, diff::State)>>,
    file: Option<LogChangeFileSelection>,
    },
    }
    #[derive(Debug)]
    pub struct LogChangeFileSelection {
    pub ix: usize,
    pub path: String,
    }
  • edit in inflorescence/src/cursor.rs at line 10
    [34.19115][34.19115:19157](),[34.19157][45.20979:21275](),[6.47][5.188:190](),[18.661][5.188:190](),[40.796][5.188:190](),[8.1691][5.188:190](),[22.3191][5.188:190](),[37.5954][5.188:190](),[41.6298][5.188:190](),[45.21275][5.188:190](),[5.188][5.188:190](),[5.190][49.2280:2281]()
    #[derive(Debug, Clone)]
    pub enum Select {
    UntrackedFile {
    ix: usize,
    path: String,
    },
    ChangedFile {
    ix: usize,
    path: String,
    },
    LogChange {
    ix: usize,
    hash: pijul::Hash,
    message: String,
    },
    LogChangeFile {
    ix: usize,
    path: String,
    },
    }
  • edit in Cargo.toml at line 41
    [53.1060]
    [2.4347]
    [workspace.dependencies.iced_test]
    git = "https://github.com/iced-rs/iced"
    rev = "50cc94d944ada88bf3d7fcd1d2741b7104b9b1d1"
  • edit in Cargo.lock at line 2397
    [2.41150]
    [2.41150]
    name = "iced_test"
    version = "0.14.0-dev"
    source = "git+https://github.com/iced-rs/iced?rev=50cc94d944ada88bf3d7fcd1d2741b7104b9b1d1#50cc94d944ada88bf3d7fcd1d2741b7104b9b1d1"
    dependencies = [
    "iced_renderer",
    "iced_runtime",
    "png",
    "sha2 0.10.8",
    "thiserror 1.0.69",
    ]
    [[package]]
  • edit in Cargo.lock at line 2734
    [52.1043]
    [52.1043]
    "iced_test",
  • replacement in Cargo.lock at line 3578
    [2.50945][31.13169:13196]()
    "proc-macro-crate 3.3.0",
    [2.50945]
    [2.50966]
    "proc-macro-crate 1.3.1",
  • replacement in Cargo.lock at line 5742
    [3.20948][3.20948:20963]()
    "rand 0.8.5",
    [3.20948]
    [3.20963]
    "rand 0.7.3",