Blob


1 #import <Cocoa/Cocoa.h>
2 #include <MacTypes.h>
3 #include <assert.h>
4 #include <mpd/client.h>
5 #include <objc/NSObjCRuntime.h>
6 #include <stdbool.h>
7 #include <stdio.h>
8 #include <stdlib.h>
9 #include <string.h>
11 #define VERSION "0.1.1"
12 #define TITLE_MAX_LENGTH 96
13 #define SLEEP_INTERVAL 0.2
15 static NSString *utf8String(const char *s) {
16 return [NSString stringWithCString:s encoding:NSUTF8StringEncoding];
17 }
19 static NSString *formatTime(unsigned int t) {
20 unsigned int hours = (t / 3600), minutes = (t % 3600 / 60),
21 seconds = (t % 60);
23 if (hours)
24 return [NSString stringWithFormat:@"%u:%02u:%02u", hours, minutes, seconds];
25 else
26 return [NSString stringWithFormat:@"%u:%02u", minutes, seconds];
27 }
29 @interface MPDController : NSObject
30 @end
32 @implementation MPDController {
33 struct mpd_connection *connection;
34 BOOL songMenuNeedsUpdate;
36 NSMenu *controlMenu;
37 NSMenuItem *timeItem, *timeSeparator, *playPauseItem, *stopItem, *nextItem,
38 *previousItem, *singleItem, *clearItem, *updateDatabaseItem,
39 *addToQueueItem;
40 NSImage *playImage, *pauseImage, *stopImage, *nextImage, *previousImage,
41 *singleImage, *clearImage;
42 NSButton *menuButton;
44 NSMenu *songMenu;
45 NSMapTable *songMap;
46 }
47 - (void)connect {
48 assert(connection == NULL);
50 connection = mpd_connection_new(NULL, 0, 0);
51 if (!connection) {
52 NSLog(@"Failed to create MPD connection");
53 exit(1);
54 }
56 songMenuNeedsUpdate = YES;
57 }
58 - (void)disconnect {
59 assert(connection != NULL);
60 mpd_connection_free(connection);
61 connection = NULL;
62 [songMap removeAllObjects];
63 [songMenu removeAllItems];
64 }
65 - (void)disableAllItems {
66 [playPauseItem setEnabled:NO];
67 [stopItem setEnabled:NO];
68 [nextItem setEnabled:NO];
69 [previousItem setEnabled:NO];
70 [singleItem setEnabled:NO];
71 [updateDatabaseItem setEnabled:NO];
72 [addToQueueItem setEnabled:NO];
73 }
74 - (void)updateLoop {
75 for (;;) {
76 [NSThread sleepForTimeInterval:SLEEP_INTERVAL];
77 if (!connection) {
78 [self disableAllItems];
79 [self showError:@"Failed to get status (is MPD running?)"];
80 [self connect];
81 }
82 if (!mpd_send_idle(connection)) {
83 [self disconnect];
84 continue;
85 }
86 enum mpd_idle mask = mpd_run_noidle(connection);
87 enum mpd_error err;
88 if (mask == 0 &&
89 (err = mpd_connection_get_error(connection)) != MPD_ERROR_SUCCESS) {
90 NSLog(@"mpd_run_idle error code %d: %s", err,
91 mpd_connection_get_error_message(connection));
92 [self disconnect];
93 continue;
94 }
96 if ((mask & MPD_IDLE_DATABASE) || songMenuNeedsUpdate) {
97 [self performSelectorOnMainThread:@selector(updateSongMenu)
98 withObject:nil
99 waitUntilDone:YES];
100 songMenuNeedsUpdate = NO;
103 [self performSelectorOnMainThread:@selector(updateControlMenu)
104 withObject:nil
105 waitUntilDone:YES];
107 [self performSelectorOnMainThread:@selector(updateStatus)
108 withObject:nil
109 waitUntilDone:YES];
112 - (void)updateControlMenu {
113 if (!connection)
114 return;
116 struct mpd_status *status = NULL;
117 struct mpd_song *song = NULL;
118 NSString *errorMsg = nil;
120 NSMutableString *output = [NSMutableString new];
122 status = mpd_run_status(connection);
123 if (!status) {
124 NSLog(@"%s", mpd_connection_get_error_message(connection));
126 [self disconnect];
127 goto cleanup;
130 enum mpd_state state = mpd_status_get_state(status);
131 enum mpd_single_state single = mpd_status_get_single_state(status);
132 bool active = (state == MPD_STATE_PLAY || state == MPD_STATE_PAUSE);
133 if (active) {
134 song = mpd_run_current_song(connection);
135 if (!song) {
136 errorMsg = @"Failed to retrieve current song";
137 goto cleanup;
140 if (mpd_connection_get_error(connection) != MPD_ERROR_SUCCESS) {
141 errorMsg = utf8String(mpd_connection_get_error_message(connection));
142 goto cleanup;
145 if (state == MPD_STATE_PAUSE)
146 [menuButton setImage:pauseImage];
147 else if (state == MPD_STATE_PLAY &&
148 (single == MPD_SINGLE_ON || single == MPD_SINGLE_ONESHOT))
149 [menuButton setImage:singleImage];
150 else
151 [menuButton setImage:nil];
153 const char *artist = mpd_song_get_tag(song, MPD_TAG_ARTIST, 0);
154 const char *title = mpd_song_get_tag(song, MPD_TAG_TITLE, 0);
156 if (artist)
157 [output appendString:utf8String(artist)];
158 if (artist && title)
159 [output appendString:@" - "];
160 if (title)
161 [output appendString:utf8String(title)];
162 if (!(artist || title))
163 [output appendString:utf8String(mpd_song_get_uri(song))];
164 } else {
165 [output setString:@"No song playing"];
166 [menuButton setImage:nil];
169 int song_pos = mpd_status_get_song_pos(status);
170 unsigned int queue_length = mpd_status_get_queue_length(status);
171 if (song_pos < 0)
172 [output appendFormat:@" (%u)", queue_length];
173 else
174 [output appendFormat:@" (%u/%u)", song_pos + 1, queue_length];
176 if ([output length] > TITLE_MAX_LENGTH) {
177 int leftCount = (TITLE_MAX_LENGTH - 3) / 2;
178 int rightCount = TITLE_MAX_LENGTH - leftCount - 3;
179 [menuButton setTitle:[@[
180 [output substringToIndex:leftCount],
181 [output substringFromIndex:[output length] - rightCount]
182 ] componentsJoinedByString:@"..."]];
183 } else
184 [menuButton setTitle:output];
186 if (state == MPD_STATE_PLAY) {
187 [playPauseItem setTitle:@"Pause"];
188 [playPauseItem setImage:pauseImage];
189 [playPauseItem setAction:@selector(pause)];
190 [playPauseItem setEnabled:YES];
191 } else {
192 [playPauseItem setTitle:@"Play"];
193 [playPauseItem setImage:playImage];
194 [playPauseItem setAction:@selector(play)];
195 [playPauseItem setEnabled:(queue_length > 0)];
197 [stopItem setEnabled:active];
198 [nextItem setEnabled:(active && (song_pos < (queue_length - 1)))];
199 [previousItem setEnabled:(active && (song_pos > 0))];
201 if (queue_length == 0 && single == MPD_SINGLE_ONESHOT) {
202 [self single_off];
203 single = MPD_SINGLE_OFF;
206 if (single == MPD_SINGLE_OFF) {
207 [singleItem setTitle:@"Pause After This Track"];
208 [singleItem setAction:@selector(single_oneshot)];
209 } else {
210 [singleItem setTitle:@"Keep Playing After This Track"];
211 [singleItem setAction:@selector(single_off)];
214 [singleItem setEnabled:(active && (song_pos < queue_length))];
215 [clearItem setEnabled:(queue_length > 0)];
216 [updateDatabaseItem setEnabled:YES];
218 cleanup:
219 if (song)
220 mpd_song_free(song);
221 if (status)
222 mpd_status_free(status);
224 if (errorMsg)
225 [self showError:errorMsg];
227 - (NSMenuItem *)addControlMenuItemWithTitle:(NSString *)title
228 image:(NSImage *)image
229 action:(SEL)selector {
230 NSMenuItem *item =
231 [controlMenu addItemWithTitle:title action:selector keyEquivalent:@""];
232 [item setTarget:self];
233 [item setEnabled:NO];
234 [item setImage:image];
236 return item;
238 - (void)initControlMenu {
239 controlMenu = [NSMenu new];
240 [controlMenu setAutoenablesItems:NO];
242 #define ICON(NAME, DESC) \
243 [NSImage imageWithSystemSymbolName:@NAME accessibilityDescription:@DESC]
245 playImage = ICON("play.fill", "Play");
246 pauseImage = ICON("pause.fill", "Pause");
247 stopImage = ICON("stop.fill", "Stop");
248 nextImage = ICON("forward.fill", "Next");
249 previousImage = ICON("backward.fill", "Previous");
250 singleImage = ICON("playpause.fill", "Single");
251 clearImage = ICON("clear.fill", "Clear");
253 timeItem = [NSMenuItem new];
254 [timeItem setEnabled:NO];
255 timeSeparator = [NSMenuItem separatorItem];
257 #define ADD_ITEM(TITLE, IMAGE, ACTION) \
258 [self addControlMenuItemWithTitle:@TITLE image:IMAGE action:@selector(ACTION)]
260 playPauseItem = ADD_ITEM("Play", playImage, play);
261 stopItem = ADD_ITEM("Stop", stopImage, stop);
262 nextItem = ADD_ITEM("Next Track", nextImage, next);
263 previousItem = ADD_ITEM("Previous Track", previousImage, previous);
264 singleItem = ADD_ITEM("Pause After This Track", singleImage, single_oneshot);
266 [controlMenu addItem:[NSMenuItem separatorItem]];
268 updateDatabaseItem = ADD_ITEM("Update Database", nil, update);
270 addToQueueItem = [controlMenu addItemWithTitle:@"Add to Queue"
271 action:nil
272 keyEquivalent:@""];
273 [addToQueueItem setSubmenu:songMenu];
274 [addToQueueItem setEnabled:NO];
276 [controlMenu addItem:[NSMenuItem separatorItem]];
278 clearItem = ADD_ITEM("Clear Queue", nil, clear);
280 [controlMenu addItem:[NSMenuItem separatorItem]];
281 [controlMenu addItemWithTitle:@"Quit MPC Bar"
282 action:@selector(terminate:)
283 keyEquivalent:@"q"];
285 NSStatusBar *bar = [NSStatusBar systemStatusBar];
286 NSStatusItem *item = [bar statusItemWithLength:NSVariableStatusItemLength];
287 menuButton = [item button];
288 [menuButton setImagePosition:NSImageLeft];
289 [item setMenu:controlMenu];
290 [self updateControlMenu];
291 [self updateStatus];
293 - (void)initSongMenu {
294 songMap = [NSMapTable new];
295 songMenu = [NSMenu new];
296 [self updateSongMenu];
298 - (void)updateSongMenu {
299 if (!connection)
300 return;
302 [songMap removeAllObjects];
303 [songMenu removeAllItems];
304 if (!mpd_send_list_all(connection, "")) {
305 [self disconnect];
306 return;
309 [addToQueueItem setEnabled:YES];
311 struct mpd_entity *entity;
312 NSMutableArray *menus = [NSMutableArray new];
313 [menus addObject:songMenu];
314 BOOL directory;
315 const char *s;
316 while ((entity = mpd_recv_entity(connection))) {
317 switch (mpd_entity_get_type(entity)) {
318 case MPD_ENTITY_TYPE_DIRECTORY:
319 directory = YES;
320 s = mpd_directory_get_path(mpd_entity_get_directory(entity));
321 break;
322 case MPD_ENTITY_TYPE_SONG:
323 directory = NO;
324 s = mpd_song_get_uri(mpd_entity_get_song(entity));
325 break;
326 default:
327 continue;
330 NSString *ss = utf8String(s);
331 NSArray *components = [ss pathComponents];
333 while ([menus count] > [components count])
334 [menus removeLastObject];
336 NSString *title =
337 directory ? [components lastObject]
338 : [[components lastObject] stringByDeletingPathExtension];
340 NSMenuItem *item = [[NSMenuItem alloc]
341 initWithTitle:[title stringByReplacingOccurrencesOfString:@":"
342 withString:@"/"]
343 action:@selector(enqueue:)
344 keyEquivalent:@""];
346 [item setTarget:self];
347 [songMap setObject:ss forKey:item];
348 [[menus lastObject] addItem:item];
349 if (directory) {
350 NSMenu *menu = [NSMenu new];
351 [item setSubmenu:menu];
352 [menus addObject:menu];
354 mpd_entity_free(entity);
357 - (instancetype)init {
358 if (self = [super init]) {
359 [self connect];
360 [self initSongMenu];
361 [self initControlMenu];
363 [[[NSThread alloc] initWithTarget:self
364 selector:@selector(updateLoop)
365 object:nil] start];
367 return self;
369 - (void)dealloc {
370 mpd_connection_free(connection);
372 - (void)showError:(NSString *)msg {
373 [menuButton setTitle:[NSString stringWithFormat:@"MPC Bar: %@", msg]];
375 - (void)play {
376 mpd_run_play(connection);
378 - (void)pause {
379 mpd_run_pause(connection, true);
381 - (void)stop {
382 mpd_run_stop(connection);
384 - (void)next {
385 mpd_run_next(connection);
387 - (void)previous {
388 mpd_run_previous(connection);
390 - (void)single_oneshot {
391 mpd_run_single_state(connection, MPD_SINGLE_ONESHOT);
393 - (void)single_off {
394 mpd_run_single_state(connection, MPD_SINGLE_OFF);
396 - (void)update {
397 mpd_run_update(connection, NULL);
399 - (void)clear {
400 mpd_run_clear(connection);
402 - (void)enqueue:(id)item {
403 mpd_run_add(connection, [[songMap objectForKey:item]
404 cStringUsingEncoding:NSUTF8StringEncoding]);
406 - (void)updateStatus {
407 struct mpd_status *status = NULL;
408 struct mpd_song *song = NULL;
410 if (connection)
411 status = mpd_run_status(connection);
413 if (!status) {
414 if (connection)
415 [self disconnect];
416 return;
419 enum mpd_state state = mpd_status_get_state(status);
420 bool active = (state == MPD_STATE_PLAY || state == MPD_STATE_PAUSE);
422 if (!active || !(song = mpd_run_current_song(connection))) {
423 if ([controlMenu indexOfItem:timeItem] >= 0)
424 [controlMenu removeItem:timeItem];
425 if ([controlMenu indexOfItem:timeSeparator] >= 0)
426 [controlMenu removeItem:timeSeparator];
427 mpd_status_free(status);
428 return;
431 unsigned int elapsed = mpd_status_get_elapsed_time(status);
432 unsigned int dur = mpd_song_get_duration(song);
433 [timeItem
434 setTitle:[NSString stringWithFormat:@"%@ / %@", formatTime(elapsed),
435 (dur > 0) ? formatTime(dur) : @"?"]];
437 if ([controlMenu indexOfItem:timeItem] < 0)
438 [controlMenu insertItem:timeItem atIndex:0];
439 if ([controlMenu indexOfItem:timeSeparator] < 0)
440 [controlMenu insertItem:timeSeparator atIndex:1];
442 mpd_song_free(song);
443 mpd_status_free(status);
445 @end
447 int main(int argc, char *argv[]) {
448 if (argc > 1 && strcmp(argv[1], "-v") == 0) {
449 puts("MPC Bar "VERSION);
450 return 0;
453 [NSApplication sharedApplication];
454 [MPDController new];
455 [NSApp run];
457 return 0;