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::player::PlayerStatus;
8use crate::domain::playlist::{Playlist, PlaylistDetail};
9use crate::domain::pin::PinnedPlaylist;
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 { OutputMode::Json } else { OutputMode::Human };
53        let table = TableConfig {
54            max_width,
55            truncate: !no_trunc,
56        };
57        Self {
58            mode,
59            user_name,
60            table,
61        }
62    }
63
64    pub fn auth_status(&self, status: AuthStatus) -> Result<()> {
65        match self.mode {
66            OutputMode::Human => human::auth_status(status),
67            OutputMode::Json => json::auth_status(status),
68        }
69    }
70
71    pub fn auth_scopes(&self, scopes: AuthScopes) -> Result<()> {
72        match self.mode {
73            OutputMode::Human => human::auth_scopes(scopes),
74            OutputMode::Json => json::auth_scopes(scopes),
75        }
76    }
77
78    pub fn player_status(&self, status: PlayerStatus) -> Result<()> {
79        match self.mode {
80            OutputMode::Human => human::player_status(status),
81            OutputMode::Json => json::player_status(status),
82        }
83    }
84
85    pub fn now_playing(&self, status: PlayerStatus) -> Result<()> {
86        match self.mode {
87            OutputMode::Human => human::now_playing(status),
88            OutputMode::Json => json::now_playing(status),
89        }
90    }
91
92    pub fn search_results(&self, results: SearchResults) -> Result<()> {
93        match self.mode {
94            OutputMode::Human => human::search_results(results, self.table),
95            OutputMode::Json => json::search_results(results),
96        }
97    }
98
99    pub fn queue(&self, now_playing_id: Option<&str>, items: Vec<Track>) -> Result<()> {
100        match self.mode {
101            OutputMode::Human => human::queue(items, now_playing_id, self.table),
102            OutputMode::Json => {
103                let items = items
104                    .into_iter()
105                    .map(|track| {
106                        let id = track.id;
107                        crate::domain::search::SearchItem {
108                            id: id.clone(),
109                            name: track.name,
110                            uri: format!("spotify:track:{}", id),
111                            kind: crate::domain::search::SearchType::Track,
112                            artists: track.artists,
113                            album: track.album,
114                            duration_ms: track.duration_ms,
115                            owner: None,
116                            score: None,
117                        }
118                    })
119                    .collect();
120                json::queue(now_playing_id, items)
121            }
122        }
123    }
124
125    pub fn recently_played(&self, now_playing_id: Option<&str>, items: Vec<SearchItem>) -> Result<()> {
126        match self.mode {
127            OutputMode::Human => human::recently_played(items, now_playing_id, self.table),
128            OutputMode::Json => json::recently_played(now_playing_id, items),
129        }
130    }
131
132    pub fn cache_status(&self, status: CacheStatus) -> Result<()> {
133        match self.mode {
134            OutputMode::Human => cache::status_human(status),
135            OutputMode::Json => cache::status_json(status),
136        }
137    }
138
139    pub fn action(&self, event: &str, message: &str) -> Result<()> {
140        match self.mode {
141            OutputMode::Human => human::action(message),
142            OutputMode::Json => json::action(event, message),
143        }
144    }
145
146    pub fn album_info(&self, album: Album) -> Result<()> {
147        match self.mode {
148            OutputMode::Human => human::album_info(album, self.table),
149            OutputMode::Json => json::album_info(album),
150        }
151    }
152
153    pub fn artist_info(&self, artist: Artist) -> Result<()> {
154        match self.mode {
155            OutputMode::Human => human::artist_info(artist),
156            OutputMode::Json => json::artist_info(artist),
157        }
158    }
159
160    pub fn playlist_list(&self, playlists: Vec<Playlist>) -> Result<()> {
161        match self.mode {
162            OutputMode::Human => {
163                human::playlist_list(playlists, self.user_name.as_deref(), self.table)
164            }
165            OutputMode::Json => json::playlist_list(playlists),
166        }
167    }
168
169    pub fn playlist_list_with_pins(
170        &self,
171        playlists: Vec<Playlist>,
172        pins: Vec<PinnedPlaylist>,
173    ) -> Result<()> {
174        match self.mode {
175            OutputMode::Human => {
176                human::playlist_list_with_pins(
177                    playlists,
178                    pins,
179                    self.user_name.as_deref(),
180                    self.table,
181                )
182            }
183            OutputMode::Json => json::playlist_list_with_pins(playlists, pins),
184        }
185    }
186
187    pub fn playlist_info(&self, playlist: PlaylistDetail) -> Result<()> {
188        match self.mode {
189            OutputMode::Human => human::playlist_info(playlist, self.user_name.as_deref()),
190            OutputMode::Json => json::playlist_info(playlist),
191        }
192    }
193
194    pub fn device_list(&self, devices: Vec<Device>) -> Result<()> {
195        match self.mode {
196            OutputMode::Human => human::device_list(devices, self.table),
197            OutputMode::Json => json::device_list(devices),
198        }
199    }
200
201    pub fn settings(&self, settings: Settings) -> Result<()> {
202        match self.mode {
203            OutputMode::Human => settings::settings_human(settings),
204            OutputMode::Json => settings::settings_json(settings),
205        }
206    }
207
208    pub fn pin_list(&self, pins: Vec<PinnedPlaylist>) -> Result<()> {
209        match self.mode {
210            OutputMode::Human => pin::pin_list_human(pins, self.table),
211            OutputMode::Json => pin::pin_list_json(pins),
212        }
213    }
214
215    pub fn help(&self) -> Result<()> {
216        match self.mode {
217            OutputMode::Human => human::help(),
218            OutputMode::Json => json::help(),
219        }
220    }
221}