spotify_cli/output/
mod.rs

1//! Output formatting for human and JSON modes.
2use crate::domain::album::Album;
3use crate::domain::artist::Artist;
4use crate::domain::auth::{AuthScopes, AuthStatus};
5use crate::domain::cache::CacheStatus;
6use crate::domain::device::Device;
7use crate::domain::pin::PinnedPlaylist;
8use crate::domain::player::PlayerStatus;
9use crate::domain::playlist::{Playlist, PlaylistDetail};
10use crate::domain::search::{SearchItem, SearchResults};
11use crate::domain::settings::Settings;
12use crate::domain::track::Track;
13use crate::error::Result;
14
15pub mod cache;
16pub mod human;
17pub mod json;
18pub mod pin;
19pub mod settings;
20
21/// Output mode for CLI responses.
22#[derive(Debug, Clone, Copy)]
23pub enum OutputMode {
24    Human,
25    Json,
26}
27
28pub const DEFAULT_MAX_WIDTH: usize = 48;
29
30/// Table rendering configuration for human output.
31#[derive(Debug, Clone, Copy)]
32pub struct TableConfig {
33    pub max_width: Option<usize>,
34    pub truncate: bool,
35}
36
37/// Unified output facade for CLI commands.
38#[derive(Debug, Clone)]
39pub struct Output {
40    mode: OutputMode,
41    user_name: Option<String>,
42    table: TableConfig,
43}
44
45impl Output {
46    pub fn new(
47        json: bool,
48        user_name: Option<String>,
49        max_width: Option<usize>,
50        no_trunc: bool,
51    ) -> Self {
52        let mode = if json {
53            OutputMode::Json
54        } else {
55            OutputMode::Human
56        };
57        let table = TableConfig {
58            max_width,
59            truncate: !no_trunc,
60        };
61        Self {
62            mode,
63            user_name,
64            table,
65        }
66    }
67
68    pub fn auth_status(&self, status: AuthStatus) -> Result<()> {
69        match self.mode {
70            OutputMode::Human => human::auth_status(status),
71            OutputMode::Json => json::auth_status(status),
72        }
73    }
74
75    pub fn auth_scopes(&self, scopes: AuthScopes) -> Result<()> {
76        match self.mode {
77            OutputMode::Human => human::auth_scopes(scopes),
78            OutputMode::Json => json::auth_scopes(scopes),
79        }
80    }
81
82    pub fn player_status(&self, status: PlayerStatus) -> Result<()> {
83        match self.mode {
84            OutputMode::Human => human::player_status(status),
85            OutputMode::Json => json::player_status(status),
86        }
87    }
88
89    pub fn now_playing(&self, status: PlayerStatus) -> Result<()> {
90        match self.mode {
91            OutputMode::Human => human::now_playing(status),
92            OutputMode::Json => json::now_playing(status),
93        }
94    }
95
96    pub fn search_results(&self, results: SearchResults) -> Result<()> {
97        match self.mode {
98            OutputMode::Human => human::search_results(results, self.table),
99            OutputMode::Json => json::search_results(results),
100        }
101    }
102
103    pub fn queue(&self, now_playing_id: Option<&str>, items: Vec<Track>) -> Result<()> {
104        match self.mode {
105            OutputMode::Human => human::queue(items, now_playing_id, self.table),
106            OutputMode::Json => {
107                let items = items
108                    .into_iter()
109                    .map(|track| {
110                        let id = track.id;
111                        crate::domain::search::SearchItem {
112                            id: id.clone(),
113                            name: track.name,
114                            uri: format!("spotify:track:{}", id),
115                            kind: crate::domain::search::SearchType::Track,
116                            artists: track.artists,
117                            album: track.album,
118                            duration_ms: track.duration_ms,
119                            owner: None,
120                            score: None,
121                        }
122                    })
123                    .collect();
124                json::queue(now_playing_id, items)
125            }
126        }
127    }
128
129    pub fn recently_played(
130        &self,
131        now_playing_id: Option<&str>,
132        items: Vec<SearchItem>,
133    ) -> Result<()> {
134        match self.mode {
135            OutputMode::Human => human::recently_played(items, now_playing_id, self.table),
136            OutputMode::Json => json::recently_played(now_playing_id, items),
137        }
138    }
139
140    pub fn cache_status(&self, status: CacheStatus) -> Result<()> {
141        match self.mode {
142            OutputMode::Human => cache::status_human(status),
143            OutputMode::Json => cache::status_json(status),
144        }
145    }
146
147    pub fn action(&self, event: &str, message: &str) -> Result<()> {
148        match self.mode {
149            OutputMode::Human => human::action(message),
150            OutputMode::Json => json::action(event, message),
151        }
152    }
153
154    pub fn album_info(&self, album: Album) -> Result<()> {
155        match self.mode {
156            OutputMode::Human => human::album_info(album, self.table),
157            OutputMode::Json => json::album_info(album),
158        }
159    }
160
161    pub fn artist_info(&self, artist: Artist) -> Result<()> {
162        match self.mode {
163            OutputMode::Human => human::artist_info(artist),
164            OutputMode::Json => json::artist_info(artist),
165        }
166    }
167
168    pub fn playlist_list(&self, playlists: Vec<Playlist>) -> Result<()> {
169        match self.mode {
170            OutputMode::Human => {
171                human::playlist_list(playlists, self.user_name.as_deref(), self.table)
172            }
173            OutputMode::Json => json::playlist_list(playlists),
174        }
175    }
176
177    pub fn playlist_list_with_pins(
178        &self,
179        playlists: Vec<Playlist>,
180        pins: Vec<PinnedPlaylist>,
181    ) -> Result<()> {
182        match self.mode {
183            OutputMode::Human => human::playlist_list_with_pins(
184                playlists,
185                pins,
186                self.user_name.as_deref(),
187                self.table,
188            ),
189            OutputMode::Json => json::playlist_list_with_pins(playlists, pins),
190        }
191    }
192
193    pub fn playlist_info(&self, playlist: PlaylistDetail) -> Result<()> {
194        match self.mode {
195            OutputMode::Human => human::playlist_info(playlist, self.user_name.as_deref()),
196            OutputMode::Json => json::playlist_info(playlist),
197        }
198    }
199
200    pub fn device_list(&self, devices: Vec<Device>) -> Result<()> {
201        match self.mode {
202            OutputMode::Human => human::device_list(devices, self.table),
203            OutputMode::Json => json::device_list(devices),
204        }
205    }
206
207    pub fn settings(&self, settings: Settings) -> Result<()> {
208        match self.mode {
209            OutputMode::Human => settings::settings_human(settings),
210            OutputMode::Json => settings::settings_json(settings),
211        }
212    }
213
214    pub fn pin_list(&self, pins: Vec<PinnedPlaylist>) -> Result<()> {
215        match self.mode {
216            OutputMode::Human => pin::pin_list_human(pins, self.table),
217            OutputMode::Json => pin::pin_list_json(pins),
218        }
219    }
220
221    pub fn help(&self) -> Result<()> {
222        match self.mode {
223            OutputMode::Human => human::help(),
224            OutputMode::Json => json::help(),
225        }
226    }
227}