use crate::route::{Dest, Route, Target, code, issues, pulls, search};
use crate::ui::page::Link;
use crate::ui::text::short_sha;
use crate::diff_screen::{DiffOf, DiffPrefs, DiffScreen};
use crate::nav::PageScreen;
use crate::state::{Cmd, Screen, Screens, State};
pub struct Tab {
pub screens: Screens,
pub forward: Vec<Screen>,
pub parked: Vec<Screen>,
}
impl Tab {
fn new(first: Screen) -> Self {
Self {
screens: Screens::new(first),
forward: Vec::new(),
parked: Vec::new(),
}
}
pub fn title(&self) -> String {
title(self.screens.last())
}
}
pub fn url(screen: &Screen) -> String {
match screen {
Screen::Page(p) => p.route.url(),
Screen::Diff(d) => d.of.url(),
}
}
pub fn title(screen: &Screen) -> String {
match screen {
Screen::Page(p) => match &p.route {
Route::Issues(issues::Issue { repo, number }) => format!("{}#{number}", repo.name),
Route::Pulls(pulls::Pr { pr, .. }) => format!("{}#{}", pr.repo.name, pr.number),
Route::Code(code::Blob { path, .. }) | Route::Code(code::Tree { path, .. })
if !path.is_empty() =>
{
path.rsplit('/').next().unwrap_or(path).to_owned()
}
Route::Code(code::Commit { oid, .. }) => short_sha(oid).to_owned(),
Route::Search(search::Search { query, .. }) => format!("⌕ {query}"),
Route::Code(code::Repo(repo)) => repo.to_string(),
route => match route.repo() {
Some(repo) => route.title().replacen(&format!("{}/", repo.owner), "", 1),
None => route.title(),
},
},
Screen::Diff(d) => match &d.of {
DiffOf::Pr(pr) => format!("{}#{} files", pr.repo.name, pr.number),
DiffOf::Commit(_, oid) => format!("{} files", short_sha(oid)),
DiffOf::Range(_, from, to) => format!("{}..{} files", short_sha(from), short_sha(to)),
},
}
}
impl State {
pub fn tab_count(&self) -> usize {
self.before.len() + 1 + self.after.len()
}
pub fn active_tab(&self) -> usize {
self.before.len()
}
pub fn tab_titles(&self) -> Vec<String> {
let current = title(self.screens.last());
let before = self.before.iter().map(Tab::title);
let after = self.after.iter().map(Tab::title);
before
.chain(std::iter::once(current))
.chain(after)
.collect()
}
fn swap_in(&mut self, next: Tab) -> Tab {
Tab {
screens: std::mem::replace(&mut self.screens, next.screens),
forward: std::mem::replace(&mut self.forward, next.forward),
parked: std::mem::replace(&mut self.parked, next.parked),
}
}
#[must_use]
pub fn open_tab(&mut self, Dest { target, anchor }: Dest) -> Vec<Cmd> {
let (first, mut cmds) = match target {
Target::Page(route) => {
let route = self.followed(route);
let cmds = self.record_visit(&route);
(Screen::Page(Box::new(PageScreen::new(route, anchor))), cmds)
}
Target::Files(of) => {
let mut screen = DiffScreen::new(of, DiffPrefs::fit(self.size.0));
screen.anchor = anchor;
(Screen::Diff(Box::new(screen)), Vec::new())
}
Target::External(_) => return self.go(target.into()),
};
let left = self.swap_in(Tab::new(first));
self.before.push(left);
cmds.extend(self.load_visible(false));
let n = self.tab_count();
self.info(format!("Opened in tab {} of {n}", self.active_tab() + 1));
cmds
}
#[must_use]
pub fn open_in_tab(&mut self) -> Vec<Cmd> {
let dest = match self.screen() {
Screen::Page(p) => match p.selected_link() {
Some(Link::Url(url)) => Dest::from_url(url),
Some(_) | None => Target::Page(p.route.clone()).into(),
},
Screen::Diff(d) => Target::Page(match &d.of {
DiffOf::Pr(pr) => Route::pr(pr.clone()),
DiffOf::Commit(repo, oid) => Route::Code(code::Commit {
repo: repo.clone(),
oid: oid.to_string(),
pr: None,
}),
DiffOf::Range(repo, from, to) => Route::Code(code::Compare {
repo: repo.clone(),
spec: format!("{from}..{to}"),
}),
})
.into(),
};
let from_tree = match self.screen() {
Screen::Diff(d) if d.focus == crate::diff_screen::Pane::Tree => Some((*d).clone()),
_ => None,
};
if let Some(screen) = from_tree {
let left = self.swap_in(Tab::new(Screen::Diff(screen)));
self.before.push(left);
if let Screen::Diff(d) = self.screen_mut() {
d.focus = crate::diff_screen::Pane::Diff;
}
self.info(format!(
"Opened in tab {} of {}",
self.active_tab() + 1,
self.tab_count()
));
return self.load_visible(false);
}
self.open_tab(dest)
}
#[must_use]
pub fn switch_to_tab(&mut self, i: usize) -> Vec<Cmd> {
if i >= self.tab_count() {
return self.just_say(format!("No tab {} open", i + 1));
}
if i == self.active_tab() {
return self.just_say(format!("Already on tab {}", i + 1));
}
self.move_to_tab(i);
self.load_visible(false)
}
#[must_use]
pub fn start_at(&mut self, dest: Dest) -> Vec<Cmd> {
if self.tab_count() == 1 {
return self.go(dest);
}
self.move_to_tab(self.tab_count() - 1);
self.open_tab(dest)
}
fn move_to_tab(&mut self, i: usize) {
while self.active_tab() < i {
let next = self.after.remove(0);
let left = self.swap_in(next);
self.before.push(left);
}
while self.active_tab() > i {
let Some(previous) = self.before.pop() else {
break;
};
let right = self.swap_in(previous);
self.after.insert(0, right);
}
}
#[must_use]
pub fn step_open_tab(&mut self, forward: bool) -> Vec<Cmd> {
let n = self.tab_count();
if n == 1 {
let open = self.first_key(crate::keymap::Action::OpenInTab);
return self.just_say(format!("Only one tab open: {open} opens another"));
}
let i = self.active_tab();
let next = if forward {
(i + 1) % n
} else {
(i + n - 1) % n
};
self.switch_to_tab(next)
}
#[must_use]
pub fn close_tab(&mut self) -> Vec<Cmd> {
let next = if self.after.is_empty() {
self.before.pop()
} else {
Some(self.after.remove(0))
};
let Some(next) = next else {
let quit = self.first_key(crate::keymap::Action::Quit);
return self.just_say(format!("This is the last tab ({quit} quits)"));
};
drop(self.swap_in(next));
self.overlay = None;
self.load_visible(false)
}
pub fn tab_urls(&self) -> Vec<String> {
let before = self.before.iter().map(|t| url(t.screens.last()));
let after = self.after.iter().map(|t| url(t.screens.last()));
before
.chain(std::iter::once(url(self.screens.last())))
.chain(after)
.collect()
}
pub fn restore_tabs(&mut self, urls: &[String]) {
let prefs = DiffPrefs::fit(self.size.0);
let mut screens = urls.iter().filter_map(|u| match Dest::from_url(u).target {
Target::Files(of) => Some(Screen::Diff(Box::new(DiffScreen::new(of, prefs)))),
Target::Page(route) => Some(Screen::Page(Box::new(PageScreen::new(route, None)))),
Target::External(_) => None,
});
let Some(first) = screens.next() else {
return;
};
self.screens = Screens::new(first);
self.forward.clear();
self.after = screens.map(Tab::new).collect();
}
}