Skip to content

Commit abfcf28

Browse files
icepumaclaude
andcommitted
feat: add shell completion generation support
- Add clap_complete dependency for completion generation - Add --completions flag to generate completions for bash, zsh, fish, powershell, and elvish - Refactor main.rs to reduce function size and improve code organization - Update README.md with shell completion installation instructions - Make subcommand optional to allow running with just --completions flag 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent f0080da commit abfcf28

File tree

5 files changed

+174
-85
lines changed

5 files changed

+174
-85
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ clap = { version = "4", features = [
7171
"unicode",
7272
"wrap_help",
7373
] }
74+
clap_complete = "4"
7475
anyhow = "1"
7576
serde = { version = "1", features = ["derive"] }
7677
serde_json = "1"

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,19 @@ Interact with track.toggl.com via terminal.
2121

2222
## Shell completions
2323

24-
WIP
24+
Generate shell completions for your shell:
25+
26+
```bash
27+
# Bash
28+
fbtoggl --completions bash > ~/.local/share/bash-completion/completions/fbtoggl
29+
30+
# Zsh
31+
fbtoggl --completions zsh > ~/.zfunc/_fbtoggl
32+
# Add to ~/.zshrc: fpath=(~/.zfunc $fpath)
33+
34+
# Fish
35+
fbtoggl --completions fish > ~/.config/fish/completions/fbtoggl.fish
36+
```
2537

2638
## Usage
2739

src/cli.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ use crate::model::Range;
22
use crate::types::TimeEntryId;
33
use chrono::{DateTime, Duration, Local};
44
use clap::{Parser, Subcommand, ValueEnum};
5+
use clap_complete::{Generator, Shell, generate};
56
use jackdauer::duration;
67
use serde::Serialize;
8+
use std::io;
79

810
pub const APP_NAME: &str = "fbtoggl";
911

@@ -18,8 +20,12 @@ pub struct Options {
1820
#[arg(long)]
1921
pub debug: bool,
2022

23+
/// Generate shell completions
24+
#[arg(long, value_enum)]
25+
pub completions: Option<Shell>,
26+
2127
#[clap(subcommand)]
22-
pub subcommand: SubCommand,
28+
pub subcommand: Option<SubCommand>,
2329
}
2430

2531
#[derive(Debug, Clone, ValueEnum)]
@@ -53,7 +59,7 @@ pub enum SubCommand {
5359
Reports(Reports),
5460
}
5561

56-
#[derive(Subcommand, Debug)]
62+
#[derive(Subcommand, Debug, Clone, Copy)]
5763
pub enum Settings {
5864
/// Initialize settings
5965
Init,
@@ -250,3 +256,7 @@ pub fn output_values_json<T: Serialize>(values: &[T]) {
250256
}
251257
}
252258
}
259+
260+
pub fn print_completions<G: Generator>(generator: G, cmd: &mut clap::Command) {
261+
generate(generator, cmd, cmd.get_name().to_owned(), &mut io::stdout());
262+
}

src/main.rs

Lines changed: 138 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
//! This application provides a command-line interface to interact with
44
//! Toggl Track's time tracking service.
55
6-
use crate::cli::{Clients, Options, SubCommand, TimeEntries};
6+
use crate::cli::{Clients, Format, Options, SubCommand, TimeEntries};
77
use crate::config::init_settings_file;
8-
use clap::Parser;
8+
use clap::{CommandFactory, Parser};
99
use cli::{Projects, Reports, Settings};
1010
use client::init_client;
1111
use report_client::init_report_client;
@@ -27,95 +27,151 @@ mod client_tests;
2727

2828
fn main() -> anyhow::Result<()> {
2929
let options = Options::parse();
30+
31+
// Handle completion generation if requested
32+
if let Some(shell) = options.completions {
33+
let mut cmd = Options::command();
34+
cli::print_completions(shell, &mut cmd);
35+
return Ok(());
36+
}
37+
3038
let format = options.format;
3139
let debug = options.debug;
3240

33-
match options.subcommand {
41+
if let Some(subcommand) = options.subcommand {
42+
execute_subcommand(subcommand, debug, &format)?;
43+
} else {
44+
eprintln!(
45+
"Error: A subcommand is required when not generating completions"
46+
);
47+
std::process::exit(1);
48+
}
49+
50+
Ok(())
51+
}
52+
53+
fn execute_subcommand(
54+
subcommand: SubCommand,
55+
debug: bool,
56+
format: &Format,
57+
) -> anyhow::Result<()> {
58+
match subcommand {
3459
SubCommand::Init => init_settings_file()?,
35-
SubCommand::Settings(action) => match action {
36-
Settings::Init => init_settings_file()?,
37-
},
38-
SubCommand::Projects(action) => match action {
39-
Projects::List(list_projects) => {
40-
let client = init_client()?;
41-
42-
commands::projects::list(
43-
debug,
44-
list_projects.include_archived,
45-
&format,
46-
&client,
47-
)?;
48-
}
49-
},
50-
SubCommand::Workspaces(_action) => {
60+
SubCommand::Settings(action) => handle_settings(action)?,
61+
SubCommand::Projects(action) => handle_projects(action, debug, format)?,
62+
SubCommand::Workspaces(_action) => handle_workspaces(debug, format)?,
63+
SubCommand::TimeEntries(action) => {
64+
handle_time_entries(action, debug, format)?;
65+
}
66+
SubCommand::Clients(action) => handle_clients(action, debug, format)?,
67+
SubCommand::Reports(action) => handle_reports(action, debug)?,
68+
}
69+
Ok(())
70+
}
71+
72+
fn handle_settings(action: Settings) -> anyhow::Result<()> {
73+
match action {
74+
Settings::Init => init_settings_file()?,
75+
}
76+
Ok(())
77+
}
78+
79+
fn handle_projects(
80+
action: Projects,
81+
debug: bool,
82+
format: &Format,
83+
) -> anyhow::Result<()> {
84+
match action {
85+
Projects::List(list_projects) => {
5186
let client = init_client()?;
87+
commands::projects::list(
88+
debug,
89+
list_projects.include_archived,
90+
format,
91+
&client,
92+
)?;
93+
}
94+
}
95+
Ok(())
96+
}
5297

53-
commands::workspaces::list(debug, &format, &client)?;
98+
fn handle_workspaces(debug: bool, format: &Format) -> anyhow::Result<()> {
99+
let client = init_client()?;
100+
commands::workspaces::list(debug, format, &client)?;
101+
Ok(())
102+
}
103+
104+
fn handle_time_entries(
105+
action: TimeEntries,
106+
debug: bool,
107+
format: &Format,
108+
) -> anyhow::Result<()> {
109+
let client = init_client()?;
110+
111+
match action {
112+
TimeEntries::Create(time_entry) => {
113+
commands::time_entries::create(debug, format, &time_entry, &client)?;
114+
}
115+
TimeEntries::List(list_time_entries) => {
116+
commands::time_entries::list(
117+
debug,
118+
format,
119+
&list_time_entries.range,
120+
list_time_entries.missing,
121+
&client,
122+
)?;
54123
}
124+
TimeEntries::Start(time_entry) => {
125+
commands::time_entries::start(debug, format, &time_entry, &client)?;
126+
}
127+
TimeEntries::Stop(time_entry) => {
128+
commands::time_entries::stop(debug, format, &time_entry, &client)?;
129+
}
130+
TimeEntries::Delete(time_entry) => {
131+
commands::time_entries::delete(debug, format, &time_entry, &client)?;
132+
}
133+
TimeEntries::Details(time_entry) => {
134+
commands::time_entries::details(debug, format, &time_entry, &client)?;
135+
}
136+
}
137+
Ok(())
138+
}
139+
140+
fn handle_clients(
141+
action: Clients,
142+
debug: bool,
143+
format: &Format,
144+
) -> anyhow::Result<()> {
145+
let client = init_client()?;
55146

56-
SubCommand::TimeEntries(action) => match action {
57-
TimeEntries::Create(time_entry) => {
58-
let client = init_client()?;
59-
commands::time_entries::create(debug, &format, &time_entry, &client)?;
60-
}
61-
TimeEntries::List(list_time_entries) => {
62-
let client = init_client()?;
63-
commands::time_entries::list(
64-
debug,
65-
&format,
66-
&list_time_entries.range,
67-
list_time_entries.missing,
68-
&client,
69-
)?;
70-
}
71-
TimeEntries::Start(time_entry) => {
72-
let client = init_client()?;
73-
commands::time_entries::start(debug, &format, &time_entry, &client)?;
74-
}
75-
TimeEntries::Stop(time_entry) => {
76-
let client = init_client()?;
77-
commands::time_entries::stop(debug, &format, &time_entry, &client)?;
78-
}
79-
TimeEntries::Delete(time_entry) => {
80-
let client = init_client()?;
81-
commands::time_entries::delete(debug, &format, &time_entry, &client)?;
82-
}
83-
TimeEntries::Details(time_entry) => {
84-
let client = init_client()?;
85-
commands::time_entries::details(debug, &format, &time_entry, &client)?;
86-
}
87-
},
88-
89-
SubCommand::Clients(action) => match action {
90-
Clients::Create(create_client) => {
91-
let client = init_client()?;
92-
commands::clients::create(debug, &format, &create_client, &client)?;
93-
}
94-
Clients::List(list_clients) => {
95-
let client = init_client()?;
96-
commands::clients::list(
97-
debug,
98-
list_clients.include_archived,
99-
&format,
100-
&client,
101-
)?;
102-
}
103-
},
104-
105-
SubCommand::Reports(action) => match action {
106-
Reports::Detailed(detailed) => {
107-
let client = init_client()?;
108-
let report_client = init_report_client()?;
109-
110-
commands::reports::detailed(
111-
debug,
112-
&client,
113-
&detailed.range,
114-
&report_client,
115-
)?;
116-
}
117-
},
147+
match action {
148+
Clients::Create(create_client) => {
149+
commands::clients::create(debug, format, &create_client, &client)?;
150+
}
151+
Clients::List(list_clients) => {
152+
commands::clients::list(
153+
debug,
154+
list_clients.include_archived,
155+
format,
156+
&client,
157+
)?;
158+
}
118159
}
160+
Ok(())
161+
}
119162

163+
fn handle_reports(action: Reports, debug: bool) -> anyhow::Result<()> {
164+
match action {
165+
Reports::Detailed(detailed) => {
166+
let client = init_client()?;
167+
let report_client = init_report_client()?;
168+
commands::reports::detailed(
169+
debug,
170+
&client,
171+
&detailed.range,
172+
&report_client,
173+
)?;
174+
}
175+
}
120176
Ok(())
121177
}

0 commit comments

Comments
 (0)