use crate::api::lists::{ListPage, MyLists, StarList};
use crate::api::model::{Capped, NodeId, RepoId};
use crate::follow::{Expectation, Shown};
use crate::keymap::Action;
use crate::nav::{Doable, row};
use crate::picker::{Choice, entry, fuzzy_score, listed};
use crate::route::Route;
use crate::state::{Cmds, State};
use crate::work::{self, Cx, Failure, Job};
use super::Page;
use crate::data::DataKey;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetLists {
pub repo: RepoId,
pub item: NodeId,
pub lists: Vec<NodeId>,
pub create: Option<String>,
pub list: String,
pub add: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteList {
pub login: String,
pub slug: String,
pub id: NodeId,
pub name: String,
}
impl SetLists {
fn said(&self) -> String {
match (&self.create, self.add) {
(Some(name), _) => format!("Made the list “{name}” and added {} to it", self.repo),
(None, true) => format!("Added {} to “{}”", self.repo, self.list),
(None, false) => format!("Took {} out of “{}”", self.repo, self.list),
}
}
fn chosen(self) -> Choice {
Choice::Step(crate::act::Step::Start(
format!("{}…", self.said()),
self.into(),
))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expected {
Membership {
repo: RepoId,
list: String,
add: bool,
},
Gone { login: String, slug: String },
}
impl Expectation for Expected {
fn needs(&self, _: &State, _: &Route) -> Vec<DataKey> {
match self {
Expected::Membership { repo, .. } => {
vec![DataKey::MyLists(repo.clone())]
}
Expected::Gone { login, .. } => {
vec![DataKey::StarLists(login.to_lowercase())]
}
}
}
fn waiting(&self) -> &'static str {
match self {
Expected::Membership { .. } => "Waiting for GitHub to show the list",
Expected::Gone { .. } => "Waiting for GitHub to show the list gone",
}
}
fn shown(&self, state: &State, _: &Route) -> Shown {
match self {
Expected::Membership { repo, list, add } => state
.picked::<MyLists>(&DataKey::MyLists(repo.clone()))
.is_some_and(|m| {
m.lists
.iter()
.any(|l| l.name == *list && l.has == Some(*add))
|| (!add && m.lists.iter().all(|l| l.name != *list))
}),
Expected::Gone { login, slug } => state
.picked::<Capped<StarList>>(&DataKey::StarLists(login.to_lowercase()))
.is_some_and(|lists| lists.iter().all(|l| l.slug != *slug)),
}
.into()
}
}
fn subject(state: &State) -> Option<RepoId> {
state.route()?.repo().cloned()
}
pub fn open(state: &mut State) -> Cmds {
let Some(repo) = subject(state) else {
return state.just_say(Action::Lists.not_here());
};
let cmds = state.ensure(DataKey::MyLists(repo.clone()), true);
cmds.and(state.open_picker(crate::picker::Kind::Site(super::Pick::Lists(repo))))
}
pub fn rows(state: &State, repo: &RepoId, q: &str) -> crate::picker::Rows {
let key = DataKey::MyLists(repo.clone());
let mine = or_return!(listed::<MyLists, _>(state, &key, "your lists"));
let ids = |except: Option<&NodeId>| -> Vec<NodeId> {
(mine.lists.iter())
.filter(|l| l.has == Some(true) && Some(&l.id) != except)
.map(|l| l.id.clone())
.collect()
};
let mut rows: Vec<_> = (mine.lists.iter())
.filter(|l| fuzzy_score(q, &l.name).is_some())
.map(|l| {
let mark = if l.has == Some(true) { "✓" } else { " " };
let noun = crate::ui::text::noun(l.count, "repository");
let mut hint = format!("{} {noun}", l.count);
if l.private {
hint.push_str(" · private");
}
let label = format!("{mark} {}", l.name);
let Some(has) = l.has else {
let hint = "too long for ghtui to say whether it's in it";
return entry(label, hint, None);
};
let mut lists = ids(Some(&l.id));
if !has {
lists.push(l.id.clone());
}
let work = SetLists {
repo: repo.clone(),
item: mine.repo.clone(),
lists,
create: None,
list: l.name.clone(),
add: !has,
};
entry(label, hint, Some(work.chosen()))
})
.collect();
let name = q.trim();
if !name.is_empty()
&& mine
.lists
.iter()
.all(|l| !l.name.eq_ignore_ascii_case(name))
{
let work = SetLists {
repo: repo.clone(),
item: mine.repo.clone(),
lists: ids(None),
create: Some(name.to_owned()),
list: name.to_owned(),
add: true,
};
let label = format!("+ Make the list “{name}” and add it");
rows.push(entry(label, "↵", Some(work.chosen())));
}
if mine.lists.left_out() > 0 {
let n = mine.lists.left_out();
rows.push(entry(
format!("{n} more lists on GitHub, not listed"),
"",
None,
));
}
if rows.is_empty() {
rows.push(entry(
"You have no lists: type a name to make one",
"",
None,
));
}
rows
}
impl Job for SetLists {
type Out = ();
async fn run(&self, cx: &Cx) -> Result<(), Failure> {
let gh = &cx.gh;
let mut lists = self.lists.clone();
if let Some(name) = &self.create {
lists.push(gh.create_list(name).await?);
}
Ok(gh.set_lists(&self.item, &lists).await?)
}
fn done(self, state: &mut State, result: Result<(), Failure>) -> Cmds {
match result {
Ok(()) => {
state.info(self.said());
if let Some(route) = state.route().cloned() {
let what = Expected::Membership {
repo: self.repo.clone(),
list: self.list,
add: self.add,
};
crate::follow::expect(state, route, what);
}
let cmds = state.ensure(DataKey::MyLists(self.repo), true);
cmds.and(state.load_visible(false))
}
Err(why) => {
state.error(refused("change your lists", &why));
Cmds::new()
}
}
}
}
fn refused(what: &str, why: &Failure) -> String {
let why = why.to_string();
if why.contains("['user']") {
crate::api::scope_missing("Changing your lists", "user")
} else {
format!("GitHub didn't {what}: {why}")
}
}
fn yours<'a>(state: &'a State, login: &str) -> Result<&'a ListPage, String> {
let list = (state.route())
.and_then(|r| r.family().paged())
.and_then(|k| state.picked::<ListPage>(&k));
let Some(list) = list else { return Err("it hasn't loaded yet".into()) };
match &state.viewer {
Some(me) if me.eq_ignore_ascii_case(login) => Ok(list),
_ => Err(format!("it's @{login}'s list, not yours")),
}
}
pub fn ask_delete(state: &mut State) -> Cmds {
let Some(route @ Route::Site(Page::StarList { login, .. })) = state.route() else {
return Cmds::new();
};
let list = yours(state, login).map(|l| l.list.clone());
let route = route.clone();
let list = or_return!(list, |why| {
state.just_say(format!("Can't delete the list: {why}"))
});
let n = list.count;
let facts = vec![(
format!(
"It has {n} {}; they stay starred. It can't be brought back",
crate::ui::text::noun(n, "repository")
),
true,
)];
crate::forms::ask(
state,
route,
format!("Delete the list “{}”?", list.name),
facts,
vec![(
"Delete list".into(),
crate::forms::Answer::Site(super::Answer::DeleteList),
)],
)
}
pub fn delete(state: &mut State, route: &Route) -> Cmds {
let Route::Site(Page::StarList { login, slug }) = route else { return Cmds::new() };
let Ok(list) = yours(state, login).map(|l| l.list.clone()) else {
return Cmds::new();
};
crate::forms::sending(state);
let delete = DeleteList {
login: login.clone(),
slug: slug.clone(),
id: list.id,
name: list.name,
};
work::start(delete)
}
impl Job for DeleteList {
type Out = ();
async fn run(&self, cx: &Cx) -> Result<(), Failure> {
Ok(cx.gh.delete_list(&self.id).await?)
}
fn done(self, state: &mut State, result: Result<(), Failure>) -> Cmds {
let d = self;
let route = Route::Site(Page::StarList {
login: d.login.clone(),
slug: d.slug.clone(),
});
match result {
Ok(()) => {
crate::forms::close_sender(state, &route);
state.info(format!("Deleted the list “{}”", d.name));
if state.route() != Some(&route) {
return Cmds::new();
}
let stars = super::stars(&d.login);
let gone = Expected::Gone {
login: d.login,
slug: d.slug,
};
crate::follow::replace(state, stars, gone)
}
Err(why) => crate::forms::say(state, refused("delete the list", &why)),
}
}
}
pub fn doable(state: &State) -> Option<Doable> {
subject(state)?;
Some(row(
"Change",
Action::Lists,
"Add the repository to your lists, or take it out",
"",
None,
))
}
pub fn doables(state: &State, route: &Route, out: &mut Vec<Doable>) {
if let Route::Site(Page::StarList { login, .. }) = route {
let why = yours(state, login).err();
out.push(row(
"Edit",
Action::DeleteComment,
"Delete the list",
"",
why,
));
}
}