use std::{
fmt::{self, Write},
fs,
io::stdout,
process::{Command, Stdio},
rc::Rc,
sync::atomic::Ordering,
};
use anyhow::{Context, Result};
use crossterm::{cursor, terminal};
use crate::{
branch::BranchList, config::Options, git_process, minibuffer::MiniBuffer, status, State, View,
};
macro_rules! commands {
($($key:literal: $cmd:tt => [$($subkey:literal: $subcmd:tt),+$(,)?]),*$(,)?) => {
paste::paste! {
#[derive(Clone, Copy, Debug)]
pub enum GexCommand { $($cmd),* }
impl GexCommand {
pub const fn commands() -> &'static [(char, Self)] {
&[$(($key, Self::$cmd)),*]
}
pub const fn subcommands(&self) -> &[(char, SubCommand)] {
match self {
$(Self::$cmd => {
&[$((
$subkey,
SubCommand::$cmd([<$cmd:lower>]::SubCommand::$subcmd)
)),*]
}),*
}
}
}
#[derive(Clone, Copy)]
pub enum SubCommand { $($cmd([<$cmd:lower>]::SubCommand)),* }
impl fmt::Display for SubCommand {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { $(Self::$cmd(subcmd) => write!(f, "{subcmd}")),* }
}
}
$(
pub mod [<$cmd:lower>] {
use std::fmt;
#[derive(Debug, Clone, Copy)]
pub enum SubCommand { $($subcmd),* }
impl fmt::Display for SubCommand {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
$(Self::$subcmd => write!(f, stringify!([<$subcmd:lower>]))),*
}
}
}
}
)*
}
}
}
commands! {
'b': Branch => ['b': Checkout, 'n': New],
'c': Commit => ['c': Commit, 'a': Amend, 'e': Extend],
'p': Push => ['p': Remote, 'f': Force],
'z': Stash => ['s': Stash, 'p': Pop],
}
const COMMIT_TEMPLATE: &str = "
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch main
# Your branch is up to date with 'origin/main'.
#
# Changes to be committed:
# new file: shell.nix
#";
fn get_commit_message(editor: &str, amend: bool) -> Result<String> {
if amend {
let output = git_process(&["log", "-1", "--pretty=%B"])?.stdout;
let last_commit = str::from_utf8(&output)?;
fs::write(
".git/COMMIT_EDITMSG",
format!("{last_commit}{COMMIT_TEMPLATE}"),
)
.context("failed to write .git/COMMIT_EDITMSG")?;
} else {
fs::write(".git/COMMIT_EDITMSG", COMMIT_TEMPLATE)
.context("failed to write .git/COMMIT_EDITMSG")?;
}
Command::new(editor)
.args([".git/COMMIT_EDITMSG"])
.stdout(Stdio::inherit())
.stdin(Stdio::inherit())
.output()
.with_context(|| format!("failed to open editor ({editor})"))?;
Ok(fs::read_to_string(".git/COMMIT_EDITMSG")
.context("failed to read .git/COMMIT_EDITMSG")?
.lines()
.filter(|l| !l.starts_with('#'))
.fold(String::new(), |mut output, l| {
let _ = writeln!(output, "{l}");
output
}))
}
impl GexCommand {
#[allow(clippy::enum_glob_use)]
pub fn handle_input(self, key: char, state: &mut State, options: &Options) -> Result<()> {
use SubCommand::*;
let State {
ref mut view,
ref mut minibuffer,
..
} = state;
let Some((_, cmd)) = self.subcommands().iter().find(|(c, _)| key == *c) else {
return Ok(());
};
match cmd {
Branch(subcmd) => {
use branch::SubCommand;
match subcmd {
SubCommand::New => {
minibuffer.get_input(
Rc::new(|input| {
if let Some(input) = input {
BranchList::checkout_new(input)?;
status::REFRESH_FLAG.store(true, Ordering::Release);
}
print!("{}", cursor::Hide);
Ok(())
}),
Some("Name for the new branch: "),
view,
View::Status,
);
}
SubCommand::Checkout => {
state.branch_list.fetch()?;
*view = View::BranchList;
}
}
}
Commit(subcmd) => {
use commit::SubCommand;
match subcmd {
SubCommand::Commit => {
crossterm::execute!(stdout(), terminal::LeaveAlternateScreen)
.context("failed to leave alternate screen")?;
let commit_msg = get_commit_message(&options.editor, false)
.context("failed to get commit message")?;
MiniBuffer::push_command_output(&git_process(&[
"commit",
"-m",
&commit_msg,
])?);
status::REFRESH_FLAG.store(true, Ordering::Release);
crossterm::execute!(stdout(), terminal::EnterAlternateScreen, cursor::Hide)
.context("failed to enter alternate screen")?;
}
SubCommand::Extend => {
MiniBuffer::push_command_output(
&Command::new("git")
.args(["commit", "--amend", "--no-edit"])
.stdout(Stdio::inherit())
.stdin(Stdio::inherit())
.output()
.context("failed to run `git commit`")?,
);
status::REFRESH_FLAG.store(true, Ordering::Release);
}
SubCommand::Amend => {
crossterm::execute!(stdout(), terminal::LeaveAlternateScreen)
.context("failed to leave alternate screen")?;
let commit_msg = get_commit_message(&options.editor, true)
.context("failed to get commit message")?;
MiniBuffer::push_command_output(&git_process(&[
"commit",
"--amend",
"-m",
&commit_msg,
])?);
status::REFRESH_FLAG.store(true, Ordering::Release);
crossterm::execute!(stdout(), terminal::EnterAlternateScreen, cursor::Hide)
.context("failed to enter alternate screen")?;
}
}
*view = View::Status;
}
Push(subcmd) => {
use push::SubCommand;
crossterm::execute!(stdout(), cursor::MoveToColumn(0), cursor::Show)?;
terminal::disable_raw_mode().context("failed to disable raw mode")?;
match subcmd {
SubCommand::Remote => MiniBuffer::push_command_output(&git_process(&["push"])?),
SubCommand::Force => {
MiniBuffer::push_command_output(&git_process(&["push", "--force"])?);
}
}
crossterm::execute!(stdout(), cursor::Hide)?;
terminal::enable_raw_mode().context("failed to enable raw mode")?;
*view = View::Status;
}
Stash(subcmd) => {
use stash::SubCommand;
match subcmd {
SubCommand::Stash => MiniBuffer::push_command_output(&git_process(&["stash"])?),
SubCommand::Pop => {
MiniBuffer::push_command_output(&git_process(&["stash", "pop"])?);
}
}
status::REFRESH_FLAG.store(true, Ordering::Release);
*view = View::Status;
}
}
Ok(())
}
}