Skip to content

Commit fd3c3bd

Browse files
rainfrog edit command for configs (#271)
* rainfrog edit command for configs * add placeholder for db configs in default config * update usage info
1 parent 8ae0dff commit fd3c3bd

File tree

5 files changed

+91
-14
lines changed

5 files changed

+91
-14
lines changed

.config/rainfrog_config.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ mouse_mode = true
33
data_compact_columns = true
44
data_row_spacer = false
55

6+
[db]
7+
# you can add frequently used database connections here. example:
8+
# dev-postgres = { host = "localhost", driver = "postgres", port = 5499, database = "rainfrog", username = "root" }
9+
610
[keybindings.Menu]
711
"<Ctrl-c>" = "Quit"
812
"q" = "AbortQuery"

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,11 @@ installation instructions: [ODPI-C Client Library Loading](https://odpi-c.readth
214214
## usage
215215

216216
```sh
217-
Usage: rainfrog [OPTIONS]
217+
Usage: rainfrog [OPTIONS] [COMMAND]
218+
219+
Commands:
220+
edit Edit the config file (create it first if missing)
221+
help Print this message or the help of the given subcommand(s)
218222

219223
Options:
220224
-M, --mouse <MOUSE_MODE> Whether to enable mouse event support. If enabled, the default mouse event handling for your terminal

src/cli.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ use crate::{
1515
#[derive(Parser, Debug, Clone)]
1616
#[command(author, version = version(), about)]
1717
pub struct Cli {
18+
#[command(subcommand)]
19+
pub command: Option<CliCommand>,
20+
1821
#[arg(
1922
short = 'M',
2023
long = "mouse",
@@ -53,6 +56,12 @@ pub struct Cli {
5356
pub connection_name: Option<String>,
5457
}
5558

59+
#[derive(clap::Subcommand, Debug, Clone, Copy, PartialEq, Eq)]
60+
pub enum CliCommand {
61+
/// Edit the config file (create it first if missing)
62+
Edit,
63+
}
64+
5665
#[derive(Parser, Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
5766
pub enum Driver {
5867
#[serde(alias = "postgres", alias = "POSTGRES")]
@@ -248,4 +257,10 @@ mod tests {
248257
assert!(err.to_string().contains("Invalid connection URL format"), "Unexpected error for {url}: {err}");
249258
}
250259
}
260+
261+
#[test]
262+
fn parses_edit_subcommand() {
263+
let cli = Cli::parse_from(["rainfrog", "edit"]);
264+
assert_eq!(cli.command, Some(CliCommand::Edit));
265+
}
251266
}

src/config.rs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ const FRAGMENT: &AsciiSet = &CONTROLS
4242
.add(b'/');
4343

4444
const CONFIG: &str = include_str!("../.config/rainfrog_config.toml");
45+
const CONFIG_FILE_CANDIDATES: [(&str, config::FileFormat); 5] = [
46+
("rainfrog_config.json5", config::FileFormat::Json5),
47+
("rainfrog_config.json", config::FileFormat::Json),
48+
("rainfrog_config.yaml", config::FileFormat::Yaml),
49+
("rainfrog_config.toml", config::FileFormat::Toml),
50+
("rainfrog_config.ini", config::FileFormat::Ini),
51+
];
52+
const PREFERRED_CONFIG_FILENAME: &str = "rainfrog_config.toml";
4553

4654
#[derive(Clone, Debug, Deserialize, Default)]
4755
pub struct AppConfig {
@@ -128,15 +136,8 @@ impl Config {
128136
.set_default("_config_dir", config_dir.to_str().unwrap())?
129137
.set_default("_favorites_dir", favorites_dir.to_str().unwrap())?;
130138

131-
let config_files = [
132-
("rainfrog_config.json5", config::FileFormat::Json5),
133-
("rainfrog_config.json", config::FileFormat::Json),
134-
("rainfrog_config.yaml", config::FileFormat::Yaml),
135-
("rainfrog_config.toml", config::FileFormat::Toml),
136-
("rainfrog_config.ini", config::FileFormat::Ini),
137-
];
138139
let mut found_config = false;
139-
for (file, format) in &config_files {
140+
for (file, format) in &CONFIG_FILE_CANDIDATES {
140141
builder = builder.add_source(config::File::from(config_dir.join(file)).format(*format).required(false));
141142
if config_dir.join(file).exists() {
142143
found_config = true
@@ -183,6 +184,19 @@ impl Config {
183184
}
184185
}
185186

187+
pub fn preferred_config_path() -> PathBuf {
188+
crate::utils::get_config_dir().join(PREFERRED_CONFIG_FILENAME)
189+
}
190+
191+
pub fn existing_config_path() -> Option<PathBuf> {
192+
let config_dir = crate::utils::get_config_dir();
193+
CONFIG_FILE_CANDIDATES.iter().map(|(name, _)| config_dir.join(name)).find(|path| path.exists())
194+
}
195+
196+
pub fn default_config_contents() -> &'static str {
197+
CONFIG
198+
}
199+
186200
#[derive(Clone, Debug, Default, Deref, DerefMut)]
187201
pub struct KeyBindings(pub HashMap<Focus, HashMap<Vec<KeyEvent>, Action>>);
188202

src/main.rs

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ pub mod utils;
1717
pub mod vim;
1818

1919
use std::{
20-
env,
20+
env, fs,
2121
io::{self, Write},
22+
path::Path,
23+
process::Command,
2224
};
2325

2426
use clap::Parser;
25-
use cli::{Cli, Driver, extract_driver_from_url, prompt_for_database_selection};
27+
use cli::{Cli, CliCommand, Driver, extract_driver_from_url, prompt_for_database_selection};
2628
use color_eyre::eyre::Result;
27-
use config::{Config, ConnectionString};
29+
use config::{Config, ConnectionString, default_config_contents, existing_config_path, preferred_config_path};
2830
use dotenvy::dotenv;
2931
use keyring::get_password;
3032

@@ -93,13 +95,51 @@ fn resolve_driver(args: &mut Cli, config: &Config) -> Result<Driver> {
9395
Ok(driver)
9496
}
9597

98+
fn ensure_config_file(path: &Path) -> Result<()> {
99+
if path.exists() {
100+
return Ok(());
101+
}
102+
103+
if let Some(parent) = path.parent() {
104+
fs::create_dir_all(parent)?;
105+
}
106+
fs::write(path, default_config_contents())?;
107+
Ok(())
108+
}
109+
110+
fn open_editor(path: &Path) -> Result<()> {
111+
let editor = env::var("VISUAL")
112+
.ok()
113+
.filter(|value| !value.trim().is_empty())
114+
.or_else(|| env::var("EDITOR").ok().filter(|value| !value.trim().is_empty()))
115+
.unwrap_or_else(|| "vi".to_string());
116+
117+
let mut parts = editor.split_whitespace();
118+
let program = parts.next().ok_or_else(|| color_eyre::eyre::eyre!("Could not parse editor command"))?;
119+
let status = Command::new(program).args(parts).arg(path).status()?;
120+
if !status.success() {
121+
color_eyre::eyre::bail!("Editor exited with status code {:?}", status.code());
122+
}
123+
Ok(())
124+
}
125+
126+
fn edit_config_file() -> Result<()> {
127+
let config_path = existing_config_path().unwrap_or_else(preferred_config_path);
128+
ensure_config_file(&config_path)?;
129+
open_editor(&config_path)
130+
}
131+
96132
async fn tokio_main() -> Result<()> {
133+
let mut args = Cli::parse();
134+
dotenv().ok();
135+
if args.command == Some(CliCommand::Edit) {
136+
return edit_config_file();
137+
}
138+
97139
initialize_logging()?;
98140

99141
initialize_panic_handler()?;
100142

101-
let mut args = Cli::parse();
102-
dotenv().ok();
103143
let config = Config::new()?;
104144
let driver = resolve_driver(&mut args, &config)?;
105145

0 commit comments

Comments
 (0)