From 38c5b465a6219205e218b639731935218a39e5ca Mon Sep 17 00:00:00 2001
From: Lars Jansen
Date: Fri, 30 Oct 2026 11:09:53 +0100
Subject: [PATCH 1/2] feat: open several repositories in tabs, remembered
between runs
---
CONTRIBUTING.md | 2 +-
README.md | 3 +-
ROADMAP.md | 34 +++++-----
crates/app/src/app.rs | 8 ++-
crates/app/src/main.rs | 12 ++--
crates/app/src/start.rs | 6 ++
crates/app/src/tests.rs | 136 +++++++++++++++++++++++++++++++++-----
crates/app/src/ui.rs | 141 +++++++++++++++++++++++++++++++---------
docs/screenshot.png | Bin 177392 -> 177563 bytes
9 files changed, 270 insertions(+), 72 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 46bee55..06ee4eb 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -66,7 +66,7 @@ Its tests run against real repositories made with `git` in a temporary folder
- `app.rs` holds the state. Views never change it directly: they push an `Action`, which
`Workspace::apply` carries out after the frame. Anything that touches the repository runs on a
background thread and is followed by a reload, so the window never waits.
-- `start.rs` (open, clone and create), `ui.rs` (toolbar), `graph.rs`, `sidebar.rs`, `details.rs`
+- `start.rs` (open, clone and create), `ui.rs` (tabs and toolbar), `graph.rs`, `sidebar.rs`, `details.rs`
and `diff.rs` draw the views, and `theme.rs` has the colours.
The app's tests (`crates/app/src/tests.rs`) drive the whole window the way a user does, through
diff --git a/README.md b/README.md
index cb9ecb5..517a985 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
-
+
## Fast
@@ -42,6 +42,7 @@ with that one.
- **Start**: open a folder, clone from a URL with git's progress as it goes, or create a new
repository.
+- **Tabs**: keep several repositories open at once; they open again where you left them.
- **Graph**: every branch, remote branch and tag in one graph, children above parents, with
coloured lanes, branch and tag labels and the uncommitted changes on top. Arrow keys walk it.
- **Commits**: message, author, parents and the files they changed, with the diff of each file.
diff --git a/ROADMAP.md b/ROADMAP.md
index f6499cd..18a9fba 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -20,29 +20,29 @@ drive it the way a user does, and keeps every benchmark as fast as before.
- [x] Stash, apply, pop and drop
- [x] Refresh when the window gets focus
- [x] Clone and init from the start screen, with progress
+- [x] Tabs: several repositories open at once, remembered between runs
## Next
-1. **Tabs**: several repositories open at once, remembered between runs.
-2. **Live refresh**: watch the working tree and refs, and reload by itself.
-3. **Discard** changes per file, hunk or line.
-4. **Confirm destructive actions**: hard reset, deleting a branch with unmerged commits, force push.
-5. **Undo and redo** of the last action, through the reflog.
-6. **Conflicts**: list conflicted files, resolve them in a three-way view (ours, theirs, result),
+1. **Live refresh**: watch the working tree and refs, and reload by itself.
+2. **Discard** changes per file, hunk or line.
+3. **Confirm destructive actions**: hard reset, deleting a branch with unmerged commits, force push.
+4. **Undo and redo** of the last action, through the reflog.
+5. **Conflicts**: list conflicted files, resolve them in a three-way view (ours, theirs, result),
mark them resolved, and continue or abort a merge, rebase or cherry-pick.
-7. **Interactive rebase**: reorder, squash, fix up, reword and drop commits, by dragging in the
+6. **Interactive rebase**: reorder, squash, fix up, reword and drop commits, by dragging in the
graph.
-8. **Credentials**: ask for a username, password or passphrase when git needs one.
-9. **Push and pull options**: force with lease, push tags, pick the remote, pull with rebase.
-10. **Remotes**: add, edit and remove.
-11. **Search the graph** by message, author or SHA, and jump to any branch or tag.
-12. **File history and blame.**
-13. **Better diffs**: side by side, word-level changes, syntax colouring, images.
-14. **Keyboard**: shortcuts for everything, and a command palette.
-15. **Settings**: light and dark theme, font size, date format, default pull mode.
-16. **Graph polish**: resizable columns, compact lanes, author avatars (optional, never by default
+7. **Credentials**: ask for a username, password or passphrase when git needs one.
+8. **Push and pull options**: force with lease, push tags, pick the remote, pull with rebase.
+9. **Remotes**: add, edit and remove.
+10. **Search the graph** by message, author or SHA, and jump to any branch or tag.
+11. **File history and blame.**
+12. **Better diffs**: side by side, word-level changes, syntax colouring, images.
+13. **Keyboard**: shortcuts for everything, and a command palette.
+14. **Settings**: light and dark theme, font size, date format, default pull mode.
+15. **Graph polish**: resizable columns, compact lanes, author avatars (optional, never by default
over the network).
-17. **Packaging**: installers with the icon for Windows (MSI), macOS (.app in a .dmg) and Linux
+16. **Packaging**: installers with the icon for Windows (MSI), macOS (.app in a .dmg) and Linux
(AppImage, .deb, Flatpak), signed where the platform expects it.
## After 1.0
diff --git a/crates/app/src/app.rs b/crates/app/src/app.rs
index f60d235..47240b3 100644
--- a/crates/app/src/app.rs
+++ b/crates/app/src/app.rs
@@ -34,8 +34,12 @@ pub enum Action {
ToggleLine(usize, usize),
/// Ask for a name first.
Prompt(Prompt),
- /// Switch to another repository.
+ /// Open a repository in a tab of its own, or show its tab if it is open.
OpenRepo(PathBuf),
+ /// Show a tab, or the start screen for `None`.
+ ShowTab(Option),
+ /// Close a tab.
+ CloseTab(usize),
}
/// What the details panel shows.
@@ -230,7 +234,7 @@ impl Workspace {
}
}
Action::Prompt(prompt) => self.prompt = Some((prompt, String::new())),
- Action::OpenRepo(_) => unreachable!("the app opens repositories"),
+ Action::OpenRepo(_) | Action::ShowTab(_) | Action::CloseTab(_) => unreachable!("the app handles tabs"),
}
}
diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs
index 87b22aa..d90d460 100644
--- a/crates/app/src/main.rs
+++ b/crates/app/src/main.rs
@@ -16,8 +16,6 @@ use std::path::PathBuf;
use eframe::egui;
-const RECENT: &str = "recent";
-
/// The logo, which is also the window icon.
const LOGO: &[u8] = include_bytes!("../../../assets/icon/256.png");
@@ -34,8 +32,9 @@ impl eframe::App for Window {
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
- let recent: Vec<_> = self.0.recent.iter().map(|path| path.display().to_string()).collect();
- storage.set_string(RECENT, recent.join("\n"));
+ for (key, value) in ui::KEYS.into_iter().zip(self.0.saved()) {
+ storage.set_string(key, value);
+ }
}
}
@@ -52,10 +51,9 @@ fn main() -> eframe::Result {
"GitCraft",
options,
Box::new(|cc| {
- let recent = cc.storage.and_then(|s| s.get_string(RECENT)).unwrap_or_default();
- let recent = recent.lines().map(PathBuf::from).collect();
theme::apply(&cc.egui_ctx);
- Ok(Box::new(Window(ui::App::new(recent, path))))
+ let app = ui::App::restore(|key| cc.storage.and_then(|s| s.get_string(key)), path);
+ Ok(Box::new(Window(app)))
}),
)
}
diff --git a/crates/app/src/start.rs b/crates/app/src/start.rs
index 3f3af92..5a003ca 100644
--- a/crates/app/src/start.rs
+++ b/crates/app/src/start.rs
@@ -25,6 +25,12 @@ pub struct Start {
}
impl Start {
+ /// Starts afresh once a repository is open, for the next new tab.
+ pub fn clear(&mut self) {
+ self.error = None;
+ self.path.clear();
+ }
+
/// Draws the start screen.
pub fn show(&mut self, ui: &mut Ui, recent: &[PathBuf], actions: &mut Vec) {
let logo = self
diff --git a/crates/app/src/tests.rs b/crates/app/src/tests.rs
index 142fd71..9113ed8 100644
--- a/crates/app/src/tests.rs
+++ b/crates/app/src/tests.rs
@@ -1,5 +1,6 @@
//! The whole app, driven through its accessibility tree against real repositories.
+use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::time::{Duration, Instant};
@@ -12,7 +13,7 @@ use tempfile::TempDir;
use crate::app::Selection;
use crate::start::percent;
-use crate::ui::App;
+use crate::ui::{App, KEYS};
/// A repository with one commit, with the tester's own git config kept out of the way.
fn repo() -> (TempDir, Git) {
@@ -37,7 +38,10 @@ fn log(git: &Git) -> String {
}
fn app(path: Option<&Path>) -> Harness<'static, App> {
- let app = App::new(Vec::new(), path.map(Path::to_path_buf));
+ run(App::restore(|_| None, path.map(Path::to_path_buf)))
+}
+
+fn run(app: App) -> Harness<'static, App> {
let mut harness =
Harness::builder().with_size([1280.0, 720.0]).build_ui_state(|ui, app: &mut App| app.show(ui), app);
settle(&mut harness);
@@ -57,10 +61,28 @@ fn settle(harness: &mut Harness<'_, App>) {
}
fn busy(app: &App) -> bool {
- match &app.workspace {
- Some(ws) => ws.busy().is_some(),
- None => app.start.dialog.as_ref().is_some_and(crate::start::Dialog::busy),
- }
+ app.tabs.iter().any(|ws| ws.busy().is_some()) || app.start.dialog.as_ref().is_some_and(crate::start::Dialog::busy)
+}
+
+/// The names on the tabs.
+fn tabs(app: &App) -> Vec {
+ app.tabs.iter().map(|ws| ws.repo.name()).collect()
+}
+
+fn name(git: &Git) -> String {
+ git.dir().file_name().unwrap().to_string_lossy().into_owned()
+}
+
+/// Opens `git` from the start screen of a new tab.
+fn open_in_new_tab(harness: &mut Harness<'_, App>, git: &Git) {
+ harness.get_by_label("New tab").click();
+ harness.run();
+ harness.get_by_role(Role::TextInput).click();
+ harness.run();
+ harness.get_by_role(Role::TextInput).type_text(git.dir().to_str().unwrap());
+ harness.run();
+ harness.key_press(Key::Enter);
+ settle(harness);
}
/// Types into the text field labelled `label`.
@@ -80,7 +102,7 @@ fn the_start_screen_offers_to_open_a_repository() {
#[test]
fn a_bad_path_explains_itself_on_the_start_screen() {
let harness = app(Some(Path::new("/surely/not/a/repository")));
- assert!(harness.state().workspace.is_none());
+ assert!(harness.state().workspace().is_none());
harness.get_by_label_contains("/surely/not/a/repository");
}
@@ -96,7 +118,7 @@ fn clone_a_repository_from_the_start_screen() {
harness.get_by_label("Clone").click();
settle(&mut harness);
- let ws = harness.state().workspace.as_ref().expect("the clone is open");
+ let ws = harness.state().workspace().expect("the clone is open");
let name = source_dir.path().file_name().unwrap();
assert_eq!(fs::canonicalize(ws.repo.git.dir()).unwrap(), fs::canonicalize(into.path().join(name)).unwrap());
harness.get_by_role_and_label(Role::Button, "First commit");
@@ -113,7 +135,7 @@ fn a_failing_clone_shows_git_s_message_and_stays_open() {
harness.get_by_label("Clone").click();
settle(&mut harness);
- assert!(harness.state().workspace.is_none());
+ assert!(harness.state().workspace().is_none());
let error = harness.state().start.dialog.as_ref().and_then(|d| d.error.clone()).expect("an error");
harness.get_by_label(&error);
harness.get_by_label("Cancel").click();
@@ -132,7 +154,7 @@ fn create_a_repository_from_the_start_screen() {
harness.get_by_label("Create").click();
settle(&mut harness);
- assert_eq!(harness.state().workspace.as_ref().expect("the new repository is open").repo.name(), "project");
+ assert_eq!(harness.state().workspace().expect("the new repository is open").repo.name(), "project");
assert!(into.path().join("project/.git").is_dir());
}
@@ -154,7 +176,7 @@ fn stage_and_commit_a_change() {
harness.run();
harness.get_by_label("Stage").click();
settle(&mut harness);
- harness.state_mut().workspace.as_mut().unwrap().summary = "Second commit".into();
+ harness.state_mut().workspace_mut().unwrap().summary = "Second commit".into();
harness.run();
harness.get_by_label("Commit changes to 1 file").click();
settle(&mut harness);
@@ -169,15 +191,15 @@ fn arrow_keys_walk_the_history() {
let (_dir, git) = repo();
commit(&git, "Second commit");
let mut harness = app(Some(git.dir()));
- let ids = harness.state().workspace.as_ref().unwrap().snapshot.history.ids.clone();
- assert_eq!(harness.state().workspace.as_ref().unwrap().selection, Some(Selection::Commit(ids[0])));
+ let ids = harness.state().workspace().unwrap().snapshot.history.ids.clone();
+ assert_eq!(harness.state().workspace().unwrap().selection, Some(Selection::Commit(ids[0])));
harness.key_press(Key::ArrowDown);
harness.run();
- assert_eq!(harness.state().workspace.as_ref().unwrap().selection, Some(Selection::Commit(ids[1])));
+ assert_eq!(harness.state().workspace().unwrap().selection, Some(Selection::Commit(ids[1])));
harness.key_press(Key::ArrowUp);
harness.run();
- assert_eq!(harness.state().workspace.as_ref().unwrap().selection, Some(Selection::Commit(ids[0])));
+ assert_eq!(harness.state().workspace().unwrap().selection, Some(Selection::Commit(ids[0])));
}
#[test]
@@ -207,3 +229,87 @@ fn a_failing_operation_shows_git_s_message() {
assert!(harness.query_by_label("Dismiss").is_none());
assert_eq!(log(&git), "First commit\n");
}
+
+#[test]
+fn open_several_repositories_in_tabs() {
+ let (_a_dir, a) = repo();
+ let (_b_dir, b) = repo();
+ commit(&b, "Only in b");
+ let mut harness = app(Some(a.dir()));
+ open_in_new_tab(&mut harness, &b);
+
+ assert_eq!(tabs(harness.state()), [name(&a), name(&b)]);
+ harness.get_by_role_and_label(Role::Button, "Only in b");
+ harness.get_by_label(&name(&a)).click();
+ settle(&mut harness);
+ assert_eq!(harness.state().active, Some(0));
+ assert!(harness.query_by_label("Only in b").is_none());
+}
+
+#[test]
+fn opening_a_repository_that_is_open_shows_its_tab() {
+ let (_a_dir, a) = repo();
+ let (_b_dir, b) = repo();
+ let mut harness = app(Some(a.dir()));
+ open_in_new_tab(&mut harness, &b);
+ open_in_new_tab(&mut harness, &a);
+
+ assert_eq!(tabs(harness.state()), [name(&a), name(&b)]);
+ assert_eq!(harness.state().active, Some(0));
+}
+
+#[test]
+fn a_tab_shows_what_changed_while_it_was_out_of_sight() {
+ let (_a_dir, a) = repo();
+ let (_b_dir, b) = repo();
+ let mut harness = app(Some(a.dir()));
+ open_in_new_tab(&mut harness, &b);
+ commit(&a, "Made elsewhere");
+ harness.get_by_label(&name(&a)).click();
+ settle(&mut harness);
+
+ harness.get_by_role_and_label(Role::Button, "Made elsewhere");
+}
+
+#[test]
+fn closing_tabs_shows_the_next_and_then_the_start_screen() {
+ let repos = [repo(), repo(), repo()];
+ let [a, b, c] = repos.each_ref().map(|(_, git)| git);
+ let mut harness = app(Some(a.dir()));
+ open_in_new_tab(&mut harness, b);
+ open_in_new_tab(&mut harness, c);
+ harness.get_by_label(&name(b)).click();
+ settle(&mut harness);
+
+ harness.get_by_label(&format!("Close {}", name(b))).click();
+ settle(&mut harness);
+ assert_eq!(tabs(harness.state()), [name(a), name(c)]);
+ assert_eq!(harness.state().active, Some(1));
+ harness.get_by_label(&format!("Close {}", name(a))).click();
+ settle(&mut harness);
+ assert_eq!(harness.state().active, Some(0));
+ harness.get_by_label(&format!("Close {}", name(c))).click();
+ settle(&mut harness);
+ assert!(harness.state().tabs.is_empty());
+ harness.get_by_label("Open a repository…");
+}
+
+#[test]
+fn tabs_open_again_on_the_next_run() {
+ let (_a_dir, a) = repo();
+ let (_b_dir, b) = repo();
+ let (_c_dir, c) = repo();
+ let mut harness = app(Some(a.dir()));
+ open_in_new_tab(&mut harness, &b);
+ harness.get_by_label(&name(&a)).click();
+ settle(&mut harness);
+ let saved: HashMap<_, _> = KEYS.into_iter().zip(harness.state().saved()).collect();
+
+ let harness = run(App::restore(|key| saved.get(key).cloned(), Some(c.dir().to_owned())));
+ assert_eq!(tabs(harness.state()), [name(&a), name(&b), name(&c)]);
+ assert_eq!(harness.state().active, Some(2));
+ let harness = run(App::restore(|key| saved.get(key).cloned(), None));
+ assert_eq!(tabs(harness.state()), [name(&a), name(&b)]);
+ assert_eq!(harness.state().active, Some(0));
+ assert_eq!(harness.state().recent, [a.dir(), b.dir()]);
+}
diff --git a/crates/app/src/ui.rs b/crates/app/src/ui.rs
index 308511c..c9ee031 100644
--- a/crates/app/src/ui.rs
+++ b/crates/app/src/ui.rs
@@ -2,7 +2,7 @@
use std::path::{Path, PathBuf};
-use eframe::egui::{self, Button, Key, Ui};
+use eframe::egui::{self, Button, Key, Role, Ui, WidgetInfo};
use gitcraft_core::git::Op;
use gitcraft_core::repo::{RefKind, Repo};
@@ -12,68 +12,157 @@ use crate::{details, diff, graph, sidebar, theme};
const RECENT: usize = 10;
+/// Where [`App::saved`] keeps the recent repositories, the open tabs and the tab on screen.
+pub const KEYS: [&str; 3] = ["recent", "tabs", "active tab"];
+
/// The whole app.
#[derive(Default)]
pub struct App {
- /// The open repository.
- pub workspace: Option,
+ /// The open repositories, one per tab.
+ pub tabs: Vec,
+ /// The tab on screen, or `None` for the start screen.
+ pub active: Option,
/// Recently opened repositories, newest first.
pub recent: Vec,
- /// The start screen, shown while no repository is open.
+ /// The start screen, shown while no tab is.
pub start: Start,
focused: bool,
- pending: Option,
+ pending: Vec,
}
impl App {
- /// Starts the app, opening `path` or else the last repository on the first frame.
- pub fn new(recent: Vec, path: Option) -> Self {
- let pending = path.or_else(|| recent.first().cloned());
+ /// Starts the app with what [`Self::saved`] stored, opening those tabs and `path` on the first frame.
+ pub fn restore(stored: impl Fn(&str) -> Option, path: Option) -> Self {
+ let [recent, tabs, active] =
+ KEYS.map(|key| stored(key).map(|v| v.lines().map(PathBuf::from).collect::>()));
+ let recent = recent.unwrap_or_default();
+ // Versions without tabs opened the last repository again.
+ let tabs = tabs.unwrap_or_else(|| recent.iter().take(1).cloned().collect());
+ let pending = tabs.into_iter().chain(active.into_iter().flatten()).chain(path).collect();
Self { recent, pending, ..Self::default() }
}
+ /// What to remember until the next run, one value for each of [`KEYS`].
+ pub fn saved(&self) -> [String; 3] {
+ let tabs = self.tabs.iter().map(|ws| ws.repo.git.dir());
+ [
+ lines(self.recent.iter().map(PathBuf::as_path)),
+ lines(tabs),
+ lines(self.workspace().map(|ws| ws.repo.git.dir())),
+ ]
+ }
+
+ /// The repository on screen.
+ pub fn workspace(&self) -> Option<&Workspace> {
+ self.tabs.get(self.active?)
+ }
+
+ /// The repository on screen.
+ pub fn workspace_mut(&mut self) -> Option<&mut Workspace> {
+ self.tabs.get_mut(self.active?)
+ }
+
fn open(&mut self, ctx: &egui::Context, path: &Path) {
match Repo::open(path) {
Ok(repo) => {
let path = repo.git.dir().to_owned();
self.recent.retain(|p| *p != path);
- self.recent.insert(0, path);
+ self.recent.insert(0, path.clone());
self.recent.truncate(RECENT);
- self.workspace = Some(Workspace::new(ctx, repo));
- self.start.error = None;
+ let tab = self.tabs.iter().position(|ws| ws.repo.git.dir() == path).unwrap_or_else(|| {
+ self.tabs.push(Workspace::new(ctx, repo));
+ self.tabs.len() - 1
+ });
+ self.active = Some(tab);
+ self.start.clear();
+ }
+ Err(error) => {
+ self.start.error = Some(format!("{}: {error}", path.display()));
+ self.active = None;
}
- Err(error) => self.start.error = Some(format!("{}: {error}", path.display())),
}
}
+ /// Closes a tab and shows the one that takes its place, or the start screen after the last.
+ fn close(&mut self, tab: usize) {
+ self.tabs.remove(tab);
+ let last = self.tabs.len().checked_sub(1);
+ // The tabs after the closed one move one to the left.
+ self.active = self.active.zip(last).map(|(active, last)| (active - usize::from(active > tab)).min(last));
+ }
+
/// Draws a frame.
pub fn show(&mut self, ui: &mut Ui) {
- if let Some(path) = self.pending.take() {
- self.open(ui.ctx(), &path);
+ let ctx = ui.ctx().clone();
+ for path in std::mem::take(&mut self.pending) {
+ self.open(&ctx, &path);
}
+ self.tabs.iter_mut().for_each(Workspace::poll);
let mut actions = Vec::new();
+ if !self.tabs.is_empty() {
+ egui::Panel::top("tabs").show(ui, |ui| tabs(ui, &self.tabs, self.active, &mut actions));
+ }
let focused = ui.input(|i| i.focused);
- match &mut self.workspace {
+ match self.active {
None => self.start.show(ui, &self.recent, &mut actions),
- Some(ws) => {
- ws.poll();
+ Some(tab) => {
if focused && !self.focused {
actions.push(Action::Refresh);
}
- workspace(ui, ws, &mut actions);
+ workspace(ui, &mut self.tabs[tab], &mut actions);
}
}
self.focused = focused;
for action in actions {
- match (action, &mut self.workspace) {
- (Action::OpenRepo(path), _) => self.open(ui.ctx(), &path),
- (action, Some(ws)) => ws.apply(ui.ctx(), action),
- (_, None) => {}
+ match action {
+ Action::OpenRepo(path) => self.open(&ctx, &path),
+ Action::CloseTab(tab) => self.close(tab),
+ Action::ShowTab(tab) => {
+ self.active = tab;
+ // It may have changed while it was out of sight.
+ if let Some(ws) = self.workspace_mut() {
+ ws.apply(&ctx, Action::Refresh);
+ }
+ }
+ action => {
+ if let Some(ws) = self.workspace_mut() {
+ ws.apply(&ctx, action);
+ }
+ }
}
}
}
}
+/// Paths one per line, the way [`App::saved`] keeps them.
+fn lines<'a>(paths: impl IntoIterator- ) -> String {
+ paths.into_iter().map(|path| path.display().to_string()).collect::>().join("\n")
+}
+
+/// The tab bar: a tab per repository, each with a close button, and a new tab.
+fn tabs(ui: &mut Ui, tabs: &[Workspace], active: Option, actions: &mut Vec) {
+ ui.horizontal(|ui| {
+ for (tab, ws) in tabs.iter().enumerate() {
+ let name = ws.repo.name();
+ let path = ws.repo.git.dir().display().to_string();
+ if ui.add(Button::selectable(active == Some(tab), &name)).on_hover_text(path).clicked() {
+ actions.push(Action::ShowTab(Some(tab)));
+ }
+ let close = ui.add(Button::new("×").small().frame(false)).on_hover_text("Close");
+ close.widget_info(|| WidgetInfo::labeled(Role::Button, true, format!("Close {name}")));
+ if close.clicked() {
+ actions.push(Action::CloseTab(tab));
+ }
+ ui.separator();
+ }
+ let new = ui.add(Button::selectable(active.is_none(), "+")).on_hover_text("New tab");
+ new.widget_info(|| WidgetInfo::labeled(Role::Button, true, "New tab"));
+ if new.clicked() {
+ actions.push(Action::ShowTab(None));
+ }
+ });
+}
+
/// Asks for a folder.
pub fn pick_folder(title: &str) -> Option {
rfd::FileDialog::new().set_title(title).pick_folder()
@@ -103,13 +192,7 @@ fn workspace(ui: &mut Ui, ws: &mut Workspace, actions: &mut Vec) {
fn toolbar(ui: &mut Ui, ws: &Workspace, actions: &mut Vec) {
let status = &ws.snapshot.status;
ui.horizontal(|ui| {
- if ui.button("Open…").clicked()
- && let Some(folder) = pick_folder("Open a repository")
- {
- actions.push(Action::OpenRepo(folder));
- }
- ui.strong(ws.repo.name());
- ui.weak(status.branch.as_deref().unwrap_or("detached HEAD"));
+ ui.strong(status.branch.as_deref().unwrap_or("detached HEAD"));
ui.separator();
let idle = ws.busy().is_none();
let count = |label: &str, n: u32| if n > 0 { format!("{label} {n}") } else { label.to_owned() };
diff --git a/docs/screenshot.png b/docs/screenshot.png
index 20f97d904769ed7569843e57f2d4e85decbc23fa..a628e6d24bcdf27613b7674cbcd9cc5fe10f46ac 100644
GIT binary patch
literal 177563
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
UZ
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