flake_edit/
cli.rs

1use std::fmt::Display;
2
3use clap::{Parser, Subcommand};
4
5#[derive(Parser, Debug)]
6#[command(author, version = CliArgs::unstable_version(), about, long_about = None)]
7#[command(name = "flake-edit")]
8#[command(next_line_help = true)]
9/// Edit your flake inputs with ease
10pub struct CliArgs {
11    /// Location of the `flake.nix` file, that will be used.
12    #[arg(long)]
13    flake: Option<String>,
14    /// Location of the `flake.lock` file. Defaults to `flake.lock` in the current directory.
15    #[arg(long)]
16    lock_file: Option<String>,
17    /// Print a diff of the changes, will not write the changes to disk.
18    #[arg(long, default_value_t = false)]
19    diff: bool,
20    /// Skip updating the lockfile after editing flake.nix.
21    #[arg(long, default_value_t = false)]
22    no_lock: bool,
23    /// Disable interactive prompts.
24    #[arg(long, default_value_t = false)]
25    non_interactive: bool,
26    /// Disable reading from and writing to the completion cache.
27    #[arg(long, default_value_t = false)]
28    no_cache: bool,
29    /// Path to a custom cache file (for testing or portable configs).
30    #[arg(long)]
31    cache: Option<String>,
32
33    #[command(subcommand)]
34    subcommand: Command,
35}
36
37#[allow(unused)]
38impl CliArgs {
39    /// Surface current version together with the current git revision and date, if available
40    fn unstable_version() -> &'static str {
41        const VERSION: &str = env!("CARGO_PKG_VERSION");
42        let date = option_env!("GIT_DATE").unwrap_or("no_date");
43        let rev = option_env!("GIT_REV").unwrap_or("no_rev");
44        // This is a memory leak, only use sparingly.
45        Box::leak(format!("{VERSION} - {date} - {rev}").into_boxed_str())
46    }
47
48    pub fn subcommand(&self) -> &Command {
49        &self.subcommand
50    }
51    pub fn list(&self) -> bool {
52        matches!(self.subcommand, Command::List { .. })
53    }
54    pub fn update(&self) -> bool {
55        matches!(self.subcommand, Command::Update { .. })
56    }
57    pub fn pin(&self) -> bool {
58        matches!(self.subcommand, Command::Pin { .. })
59    }
60    pub fn unpin(&self) -> bool {
61        matches!(self.subcommand, Command::Unpin { .. })
62    }
63    pub fn change(&self) -> bool {
64        matches!(self.subcommand, Command::Change { .. })
65    }
66    pub fn follow(&self) -> bool {
67        matches!(self.subcommand, Command::Follow { .. })
68    }
69
70    pub fn flake(&self) -> Option<&String> {
71        self.flake.as_ref()
72    }
73
74    pub fn lock_file(&self) -> Option<&String> {
75        self.lock_file.as_ref()
76    }
77
78    pub fn diff(&self) -> bool {
79        self.diff
80    }
81
82    pub fn no_lock(&self) -> bool {
83        self.no_lock
84    }
85
86    pub fn non_interactive(&self) -> bool {
87        self.non_interactive
88    }
89
90    pub fn no_cache(&self) -> bool {
91        self.no_cache
92    }
93
94    pub fn cache(&self) -> Option<&String> {
95        self.cache.as_ref()
96    }
97}
98
99#[derive(Subcommand, Debug)]
100pub enum Command {
101    /// Add a new flake reference.
102    #[clap(alias = "a")]
103    Add {
104        /// The name of an input attribute.
105        id: Option<String>,
106        /// The uri that should be added to the input.
107        uri: Option<String>,
108        #[arg(long)]
109        /// Pin to a specific ref_or_rev
110        ref_or_rev: Option<String>,
111        /// The input itself is not a flake.
112        #[arg(long, short)]
113        no_flake: bool,
114        /// Use shallow clone for the input.
115        #[arg(long, short)]
116        shallow: bool,
117    },
118    /// Remove a specific flake reference based on its id.
119    #[clap(alias = "rm")]
120    Remove { id: Option<String> },
121    /// Change an existing flake reference's URI.
122    #[clap(alias = "c")]
123    Change {
124        /// The name of an existing input attribute.
125        id: Option<String>,
126        /// The new URI for the input.
127        uri: Option<String>,
128        #[arg(long)]
129        /// Pin to a specific ref_or_rev
130        ref_or_rev: Option<String>,
131        /// Use shallow clone for the input.
132        #[arg(long, short)]
133        shallow: bool,
134    },
135    /// List flake inputs
136    #[clap(alias = "l")]
137    List {
138        #[arg(long, default_value_t = ListFormat::default())]
139        format: ListFormat,
140    },
141    /// Update inputs to their latest specified release.
142    #[clap(alias = "u")]
143    Update {
144        /// The id of an input attribute.
145        /// If omitted will update all inputs.
146        id: Option<String>,
147        /// Whether the latest semver release of the remote should be used even thought the release
148        /// itself isn't yet pinned to a specific release.
149        #[arg(long)]
150        init: bool,
151    },
152    /// Pin inputs to their current or a specified rev.
153    #[clap(alias = "p")]
154    Pin {
155        /// The id of an input attribute.
156        id: Option<String>,
157        /// Optionally specify a rev for the inputs attribute.
158        rev: Option<String>,
159    },
160    /// Unpin an input so it tracks the upstream default again.
161    #[clap(alias = "up")]
162    Unpin {
163        /// The id of an input attribute.
164        id: Option<String>,
165    },
166    /// Add a follows relationship to make an input's dependency follow a top-level input.
167    ///
168    /// Example: `flake-edit follow rust-overlay.nixpkgs nixpkgs`
169    ///
170    /// This creates: `rust-overlay.inputs.nixpkgs.follows = "nixpkgs";`
171    ///
172    /// Without arguments, starts an interactive selection.
173    #[clap(alias = "f")]
174    Follow {
175        /// The input path in dot notation (e.g., "rust-overlay.nixpkgs" means
176        /// the nixpkgs input of rust-overlay).
177        input: Option<String>,
178        /// The target input to follow (e.g., "nixpkgs").
179        target: Option<String>,
180        /// Automatically follow inputs when their nested input names match top-level inputs.
181        #[arg(long, short)]
182        auto: bool,
183    },
184    #[clap(hide = true)]
185    #[command(name = "completion")]
186    /// Meant for shell completions.
187    Completion {
188        #[arg(long)]
189        inputs: bool,
190        mode: CompletionMode,
191    },
192}
193
194#[derive(Debug, Clone, Default)]
195/// Which command should be completed
196pub enum CompletionMode {
197    #[default]
198    None,
199    Add,
200    Change,
201    Follow,
202}
203
204impl From<String> for CompletionMode {
205    fn from(value: String) -> Self {
206        use CompletionMode::*;
207        match value.to_lowercase().as_str() {
208            "add" => Add,
209            "change" => Change,
210            "follow" => Follow,
211            _ => None,
212        }
213    }
214}
215
216#[derive(Debug, Clone, Default)]
217pub enum ListFormat {
218    None,
219    Simple,
220    Toplevel,
221    #[default]
222    Detailed,
223    Raw,
224    Json,
225}
226
227impl From<String> for ListFormat {
228    fn from(value: String) -> Self {
229        use ListFormat::*;
230        match value.to_lowercase().as_str() {
231            "detailed" => Detailed,
232            "simple" => Simple,
233            "toplevel" => Toplevel,
234            "raw" => Raw,
235            "json" => Json,
236            _ => None,
237        }
238    }
239}
240
241impl Display for ListFormat {
242    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243        match self {
244            ListFormat::None => write!(f, ""),
245            ListFormat::Simple => write!(f, "simple"),
246            ListFormat::Toplevel => write!(f, "toplevel"),
247            ListFormat::Detailed => write!(f, "detailed"),
248            ListFormat::Raw => write!(f, "raw"),
249            ListFormat::Json => write!(f, "json"),
250        }
251    }
252}