text:
/*
 * iTunes.h
 */

#import <AppKit/AppKit.h>
#import <ScriptingBridge/ScriptingBridge.h>

@class iTunesApplication, iTunesItem, iTunesAirPlayDevice, iTunesArtwork, iTunesEncoder, iTunesEQPreset, iTunesPlaylist, iTunesAudioCDPlaylist, iTunesLibraryPlaylist, iTunesRadioTunerPlaylist, iTunesSource, iTunesSubscriptionPlaylist, iTunesTrack, iTunesAudioCDTrack, iTunesFileTrack, iTunesSharedTrack, iTunesURLTrack, iTunesUserPlaylist, iTunesFolderPlaylist, iTunesVisual, iTunesWindow, iTunesBrowserWindow, iTunesEQWindow, iTunesMiniplayerWindow, iTunesPlaylistWindow, iTunesVideoWindow;

enum iTunesEKnd {
	iTunesEKndTrackListing = 'kTrk' /* a basic listing of tracks within a playlist */,
	iTunesEKndAlbumListing = 'kAlb' /* a listing of a playlist grouped by album */,
	iTunesEKndCdInsert = 'kCDi' /* a printout of the playlist for jewel case inserts */
};
typedef enum iTunesEKnd iTunesEKnd;

enum iTunesEnum {
	iTunesEnumStandard = 'lwst' /* Standard PostScript error handling */,
	iTunesEnumDetailed = 'lwdt' /* print a detailed report of PostScript errors */
};
typedef enum iTunesEnum iTunesEnum;

enum iTunesEPlS {
	iTunesEPlSStopped = 'kPSS',
	iTunesEPlSPlaying = 'kPSP',
	iTunesEPlSPaused = 'kPSp',
	iTunesEPlSFastForwarding = 'kPSF',
	iTunesEPlSRewinding = 'kPSR'
};
typedef enum iTunesEPlS iTunesEPlS;

enum iTunesERpt {
	iTunesERptOff = 'kRpO',
	iTunesERptOne = 'kRp1',
	iTunesERptAll = 'kAll'
};
typedef enum iTunesERpt iTunesERpt;

enum iTunesEShM {
	iTunesEShMSongs = 'kShS',
	iTunesEShMAlbums = 'kShA',
	iTunesEShMGroupings = 'kShG'
};
typedef enum iTunesEShM iTunesEShM;

enum iTunesEVSz {
	iTunesEVSzSmall = 'kVSS',
	iTunesEVSzMedium = 'kVSM',
	iTunesEVSzLarge = 'kVSL'
};
typedef enum iTunesEVSz iTunesEVSz;

enum iTunesESrc {
	iTunesESrcLibrary = 'kLib',
	iTunesESrcIPod = 'kPod',
	iTunesESrcAudioCD = 'kACD',
	iTunesESrcMP3CD = 'kMCD',
	iTunesESrcRadioTuner = 'kTun',
	iTunesESrcSharedLibrary = 'kShd',
	iTunesESrcITunesStore = 'kITS',
	iTunesESrcUnknown = 'kUnk'
};
typedef enum iTunesESrc iTunesESrc;

enum iTunesESrA {
	iTunesESrAAlbums = 'kSrL' /* albums only */,
	iTunesESrAAll = 'kAll' /* all text fields */,
	iTunesESrAArtists = 'kSrR' /* artists only */,
	iTunesESrAComposers = 'kSrC' /* composers only */,
	iTunesESrADisplayed = 'kSrV' /* visible text fields */,
	iTunesESrASongs = 'kSrS' /* song names only */
};
typedef enum iTunesESrA iTunesESrA;

enum iTunesESpK {
	iTunesESpKNone = 'kNon',
	iTunesESpKBooks = 'kSpA',
	iTunesESpKFolder = 'kSpF',
	iTunesESpKGenius = 'kSpG',
	iTunesESpKITunesU = 'kSpU',
	iTunesESpKLibrary = 'kSpL',
	iTunesESpKMovies = 'kSpI',
	iTunesESpKMusic = 'kSpZ',
	iTunesESpKPodcasts = 'kSpP',
	iTunesESpKPurchasedMusic = 'kSpM',
	iTunesESpKTVShows = 'kSpT'
};
typedef enum iTunesESpK iTunesESpK;

enum iTunesEMdK {
	iTunesEMdKAlertTone = 'kMdL' /* alert tone track */,
	iTunesEMdKAudiobook = 'kMdA' /* audiobook track */,
	iTunesEMdKBook = 'kMdB' /* book track */,
	iTunesEMdKHomeVideo = 'kVdH' /* home video track */,
	iTunesEMdKITunesU = 'kMdI' /* iTunes U track */,
	iTunesEMdKMovie = 'kVdM' /* movie track */,
	iTunesEMdKSong = 'kMdS' /* music track */,
	iTunesEMdKMusicVideo = 'kVdV' /* music video track */,
	iTunesEMdKPodcast = 'kMdP' /* podcast track */,
	iTunesEMdKRingtone = 'kMdR' /* ringtone track */,
	iTunesEMdKTVShow = 'kVdT' /* TV show track */,
	iTunesEMdKVoiceMemo = 'kMdO' /* voice memo track */,
	iTunesEMdKUnknown = 'kUnk'
};
typedef enum iTunesEMdK iTunesEMdK;

enum iTunesEVdK {
	iTunesEVdKNone = 'kNon' /* not a video or unknown video kind */,
	iTunesEVdKHomeVideo = 'kVdH' /* home video track */,
	iTunesEVdKMovie = 'kVdM' /* movie track */,
	iTunesEVdKMusicVideo = 'kVdV' /* music video track */,
	iTunesEVdKTVShow = 'kVdT' /* TV show track */
};
typedef enum iTunesEVdK iTunesEVdK;

enum iTunesERtK {
	iTunesERtKUser = 'kRtU' /* user-specified rating */,
	iTunesERtKComputed = 'kRtC' /* iTunes-computed rating */
};
typedef enum iTunesERtK iTunesERtK;

enum iTunesEAPD {
	iTunesEAPDComputer = 'kAPC',
	iTunesEAPDAirPortExpress = 'kAPX',
	iTunesEAPDAppleTV = 'kAPT',
	iTunesEAPDAirPlayDevice = 'kAPO',
	iTunesEAPDUnknown = 'kAPU'
};
typedef enum iTunesEAPD iTunesEAPD;

enum iTunesEClS {
	iTunesEClSUnknown = 'kUnk',
	iTunesEClSPurchased = 'kPur',
	iTunesEClSMatched = 'kMat',
	iTunesEClSUploaded = 'kUpl',
	iTunesEClSIneligible = 'kRej',
	iTunesEClSRemoved = 'kRem',
	iTunesEClSError = 'kErr',
	iTunesEClSDuplicate = 'kDup',
	iTunesEClSSubscription = 'kSub',
	iTunesEClSNoLongerAvailable = 'kRev',
	iTunesEClSNotUploaded = 'kUpP'
};
typedef enum iTunesEClS iTunesEClS;

@protocol iTunesGenericMethods

- (void) printPrintDialog:(BOOL)printDialog withProperties:(NSDictionary *)withProperties kind:(iTunesEKnd)kind theme:(NSString *)theme;  // Print the specified object(s)
- (void) close;  // Close an object
- (void) delete;  // Delete an element from an object
- (SBObject *) duplicateTo:(SBObject *)to;  // Duplicate one or more object(s)
- (BOOL) exists;  // Verify if an object exists
- (void) open;  // Open the specified object(s)
- (void) save;  // Save the specified object(s)
- (void) playOnce:(BOOL)once;  // play the current track or the specified track or file.
- (void) select;  // select the specified object(s)

@end

/*
 * iTunes Suite
 */

// The application program
@interface iTunesApplication : SBApplication

- (SBElementArray<iTunesAirPlayDevice *> *) AirPlayDevices;
- (SBElementArray<iTunesBrowserWindow *> *) browserWindows;
- (SBElementArray<iTunesEncoder *> *) encoders;
- (SBElementArray<iTunesEQPreset *> *) EQPresets;
- (SBElementArray<iTunesEQWindow *> *) EQWindows;
- (SBElementArray<iTunesMiniplayerWindow *> *) miniplayerWindows;
- (SBElementArray<iTunesPlaylist *> *) playlists;
- (SBElementArray<iTunesPlaylistWindow *> *) playlistWindows;
- (SBElementArray<iTunesSource *> *) sources;
- (SBElementArray<iTunesTrack *> *) tracks;
- (SBElementArray<iTunesVideoWindow *> *) videoWindows;
- (SBElementArray<iTunesVisual *> *) visuals;
- (SBElementArray<iTunesWindow *> *) windows;

@property (readonly) BOOL AirPlayEnabled;  // is AirPlay currently enabled?
@property (readonly) BOOL converting;  // is a track currently being converted?
@property (copy) NSArray<iTunesAirPlayDevice *> *currentAirPlayDevices;  // the currently selected AirPlay device(s)
@property (copy) iTunesEncoder *currentEncoder;  // the currently selected encoder (MP3, AIFF, WAV, etc.)
@property (copy) iTunesEQPreset *currentEQPreset;  // the currently selected equalizer preset
@property (copy, readonly) iTunesPlaylist *currentPlaylist;  // the playlist containing the currently targeted track
@property (copy, readonly) NSString *currentStreamTitle;  // the name of the current song in the playing stream (provided by streaming server)
@property (copy, readonly) NSString *currentStreamURL;  // the URL of the playing stream or streaming web site (provided by streaming server)
@property (copy, readonly) iTunesTrack *currentTrack;  // the current targeted track
@property (copy) iTunesVisual *currentVisual;  // the currently selected visual plug-in
@property BOOL EQEnabled;  // is the equalizer enabled?
@property BOOL fixedIndexing;  // true if all AppleScript track indices should be independent of the play order of the owning playlist.
@property BOOL frontmost;  // is iTunes the frontmost application?
@property BOOL fullScreen;  // are visuals displayed using the entire screen?
@property (copy, readonly) NSString *name;  // the name of the application
@property BOOL mute;  // has the sound output been muted?
@property double playerPosition;  // the player’s position within the currently playing track in seconds.
@property (readonly) iTunesEPlS playerState;  // is iTunes stopped, paused, or playing?
@property (copy, readonly) SBObject *selection;  // the selection visible to the user
@property BOOL shuffleEnabled;  // are songs played in random order?
@property iTunesEShM shuffleMode;  // the playback shuffle mode
@property iTunesERpt songRepeat;  // the playback repeat mode
@property NSInteger soundVolume;  // the sound output volume (0 = minimum, 100 = maximum)
@property (copy, readonly) NSString *version;  // the version of iTunes
@property BOOL visualsEnabled;  // are visuals currently being displayed?
@property iTunesEVSz visualSize;  // the size of the displayed visual

- (void) printPrintDialog:(BOOL)printDialog withProperties:(NSDictionary *)withProperties kind:(iTunesEKnd)kind theme:(NSString *)theme;  // Print the specified object(s)
- (void) run;  // Run iTunes
- (void) quit;  // Quit iTunes
- (iTunesTrack *) add:(NSArray<NSURL *> *)x to:(SBObject *)to;  // add one or more files to a playlist
- (void) backTrack;  // reposition to beginning of current track or go to previous track if already at start of current track
- (iTunesTrack *) convert:(NSArray<SBObject *> *)x;  // convert one or more files or tracks
- (void) eject;  // eject the specified iPod
- (void) fastForward;  // skip forward in a playing track
- (void) nextTrack;  // advance to the next track in the current playlist
- (void) pause;  // pause playback
- (void) playOnce:(BOOL)once;  // play the current track or the specified track or file.
- (void) playpause;  // toggle the playing/paused state of the current track
- (void) previousTrack;  // return to the previous track in the current playlist
- (void) resume;  // disable fast forward/rewind and resume playback, if playing.
- (void) rewind;  // skip backwards in a playing track
- (void) stop;  // stop playback
- (void) subscribe:(NSString *)x;  // subscribe to a podcast feed
- (void) update;  // update the specified iPod
- (void) updateAllPodcasts;  // update all subscribed podcast feeds
- (void) updatePodcast;  // update podcast feed
- (void) openLocation:(NSString *)x;  // Opens a Music Store or audio stream URL

@end

// an item
@interface iTunesItem : SBObject <iTunesGenericMethods>

@property (copy, readonly) SBObject *container;  // the container of the item
- (NSInteger) id;  // the id of the item
@property (readonly) NSInteger index;  // The index of the item in internal application order.
@property (copy) NSString *name;  // the name of the item
@property (copy, readonly) NSString *persistentID;  // the id of the item as a hexadecimal string. This id does not change over time.
@property (copy) NSDictionary *properties;  // every property of the item

- (void) download;  // download a cloud track or playlist, or a podcast episode
- (void) reveal;  // reveal and select a track or playlist

@end

// an AirPlay device
@interface iTunesAirPlayDevice : iTunesItem

@property (readonly) BOOL active;  // is the device currently being played to?
@property (readonly) BOOL available;  // is the device currently available?
@property (readonly) iTunesEAPD kind;  // the kind of the device
@property (copy, readonly) NSString *networkAddress;  // the network (MAC) address of the device
- (BOOL) protected;  // is the device password- or passcode-protected?
@property BOOL selected;  // is the device currently selected?
@property (readonly) BOOL supportsAudio;  // does the device support audio playback?
@property (readonly) BOOL supportsVideo;  // does the device support video playback?
@property NSInteger soundVolume;  // the output volume for the device (0 = minimum, 100 = maximum)

@end

// a piece of art within a track or playlist
@interface iTunesArtwork : iTunesItem

@property (copy) NSImage *data;  // data for this artwork, in the form of a picture
@property (copy) NSString *objectDescription;  // description of artwork as a string
@property (readonly) BOOL downloaded;  // was this artwork downloaded by iTunes?
@property (copy, readonly) NSNumber *format;  // the data format for this piece of artwork
@property NSInteger kind;  // kind or purpose of this piece of artwork
@property (copy) NSData *rawData;  // data for this artwork, in original format

@end

// converts a track to a specific file format
@interface iTunesEncoder : iTunesItem

@property (copy, readonly) NSString *format;  // the data format created by the encoder

@end

// equalizer preset configuration
@interface iTunesEQPreset : iTunesItem

@property double band1;  // the equalizer 32 Hz band level (-12.0 dB to +12.0 dB)
@property double band2;  // the equalizer 64 Hz band level (-12.0 dB to +12.0 dB)
@property double band3;  // the equalizer 125 Hz band level (-12.0 dB to +12.0 dB)
@property double band4;  // the equalizer 250 Hz band level (-12.0 dB to +12.0 dB)
@property double band5;  // the equalizer 500 Hz band level (-12.0 dB to +12.0 dB)
@property double band6;  // the equalizer 1 kHz band level (-12.0 dB to +12.0 dB)
@property double band7;  // the equalizer 2 kHz band level (-12.0 dB to +12.0 dB)
@property double band8;  // the equalizer 4 kHz band level (-12.0 dB to +12.0 dB)
@property double band9;  // the equalizer 8 kHz band level (-12.0 dB to +12.0 dB)
@property double band10;  // the equalizer 16 kHz band level (-12.0 dB to +12.0 dB)
@property (readonly) BOOL modifiable;  // can this preset be modified?
@property double preamp;  // the equalizer preamp level (-12.0 dB to +12.0 dB)
@property BOOL updateTracks;  // should tracks which refer to this preset be updated when the preset is renamed or deleted?

@end

// a list of songs/streams
@interface iTunesPlaylist : iTunesItem

- (SBElementArray<iTunesTrack *> *) tracks;
- (SBElementArray<iTunesArtwork *> *) artworks;

@property (copy) NSString *objectDescription;  // the description of the playlist
@property BOOL disliked;  // is this playlist disliked?
@property (readonly) NSInteger duration;  // the total length of all songs (in seconds)
@property (copy) NSString *name;  // the name of the playlist
@property BOOL loved;  // is this playlist loved?
@property (copy, readonly) iTunesPlaylist *parent;  // folder which contains this playlist (if any)
@property BOOL shuffle;  // play the songs in this playlist in random order? (obsolete; always false)
@property (readonly) NSInteger size;  // the total size of all songs (in bytes)
@property iTunesERpt songRepeat;  // playback repeat mode (obsolete; always off)
@property (readonly) iTunesESpK specialKind;  // special playlist kind
@property (copy, readonly) NSString *time;  // the length of all songs in MM:SS format
@property (readonly) BOOL visible;  // is this playlist visible in the Source list?

- (void) moveTo:(SBObject *)to;  // Move playlist(s) to a new location
- (iTunesTrack *) searchFor:(NSString *)for_ only:(iTunesESrA)only;  // search a playlist for tracks matching the search string. Identical to entering search text in the Search field in iTunes.

@end

// a playlist representing an audio CD
@interface iTunesAudioCDPlaylist : iTunesPlaylist

- (SBElementArray<iTunesAudioCDTrack *> *) audioCDTracks;

@property (copy) NSString *artist;  // the artist of the CD
@property BOOL compilation;  // is this CD a compilation album?
@property (copy) NSString *composer;  // the composer of the CD
@property NSInteger discCount;  // the total number of discs in this CD’s album
@property NSInteger discNumber;  // the index of this CD disc in the source album
@property (copy) NSString *genre;  // the genre of the CD
@property NSInteger year;  // the year the album was recorded/released

@end

// the master music library playlist
@interface iTunesLibraryPlaylist : iTunesPlaylist

- (SBElementArray<iTunesFileTrack *> *) fileTracks;
- (SBElementArray<iTunesURLTrack *> *) URLTracks;
- (SBElementArray<iTunesSharedTrack *> *) sharedTracks;

@end

// the radio tuner playlist
@interface iTunesRadioTunerPlaylist : iTunesPlaylist

- (SBElementArray<iTunesURLTrack *> *) URLTracks;

@end

// a music source (music library, CD, device, etc.)
@interface iTunesSource : iTunesItem

- (SBElementArray<iTunesAudioCDPlaylist *> *) audioCDPlaylists;
- (SBElementArray<iTunesLibraryPlaylist *> *) libraryPlaylists;
- (SBElementArray<iTunesPlaylist *> *) playlists;
- (SBElementArray<iTunesRadioTunerPlaylist *> *) radioTunerPlaylists;
- (SBElementArray<iTunesSubscriptionPlaylist *> *) subscriptionPlaylists;
- (SBElementArray<iTunesUserPlaylist *> *) userPlaylists;

@property (readonly) long long capacity;  // the total size of the source if it has a fixed size
@property (readonly) long long freeSpace;  // the free space on the source if it has a fixed size
@property (readonly) iTunesESrc kind;

- (void) eject;  // eject the specified iPod
- (void) update;  // update the specified iPod

@end

// a subscription playlist from Apple Music
@interface iTunesSubscriptionPlaylist : iTunesPlaylist

- (SBElementArray<iTunesFileTrack *> *) fileTracks;
- (SBElementArray<iTunesURLTrack *> *) URLTracks;

@end

// playable audio source
@interface iTunesTrack : iTunesItem

- (SBElementArray<iTunesArtwork *> *) artworks;

@property (copy) NSString *album;  // the album name of the track
@property (copy) NSString *albumArtist;  // the album artist of the track
@property BOOL albumDisliked;  // is the album for this track disliked?
@property BOOL albumLoved;  // is the album for this track loved?
@property NSInteger albumRating;  // the rating of the album for this track (0 to 100)
@property (readonly) iTunesERtK albumRatingKind;  // the rating kind of the album rating for this track
@property (copy) NSString *artist;  // the artist/source of the track
@property (readonly) NSInteger bitRate;  // the bit rate of the track (in kbps)
@property double bookmark;  // the bookmark time of the track in seconds
@property BOOL bookmarkable;  // is the playback position for this track remembered?
@property NSInteger bpm;  // the tempo of this track in beats per minute
@property (copy) NSString *category;  // the category of the track
@property (readonly) iTunesEClS cloudStatus;  // the iCloud status of the track
@property (copy) NSString *comment;  // freeform notes about the track
@property BOOL compilation;  // is this track from a compilation album?
@property (copy) NSString *composer;  // the composer of the track
@property (readonly) NSInteger databaseID;  // the common, unique ID for this track. If two tracks in different playlists have the same database ID, they are sharing the same data.
@property (copy, readonly) NSDate *dateAdded;  // the date the track was added to the playlist
@property (copy) NSString *objectDescription;  // the description of the track
@property NSInteger discCount;  // the total number of discs in the source album
@property NSInteger discNumber;  // the index of the disc containing this track on the source album
@property BOOL disliked;  // is this track disliked?
@property (copy, readonly) NSString *downloaderAppleID;  // the Apple ID of the person who downloaded this track
@property (copy, readonly) NSString *downloaderName;  // the name of the person who downloaded this track
@property (readonly) double duration;  // the length of the track in seconds
@property BOOL enabled;  // is this track checked for playback?
@property (copy) NSString *episodeID;  // the episode ID of the track
@property NSInteger episodeNumber;  // the episode number of the track
@property (copy) NSString *EQ;  // the name of the EQ preset of the track
@property double finish;  // the stop time of the track in seconds
@property BOOL gapless;  // is this track from a gapless album?
@property (copy) NSString *genre;  // the music/audio genre (category) of the track
@property (copy) NSString *grouping;  // the grouping (piece) of the track. Generally used to denote movements within a classical work.
@property (copy, readonly) NSString *kind;  // a text description of the track
@property (copy) NSString *longDescription;
@property BOOL loved;  // is this track loved?
@property (copy) NSString *lyrics;  // the lyrics of the track
@property iTunesEMdK mediaKind;  // the media kind of the track
@property (copy, readonly) NSDate *modificationDate;  // the modification date of the content of this track
@property (copy) NSString *movement;  // the movement name of the track
@property NSInteger movementCount;  // the total number of movements in the work
@property NSInteger movementNumber;  // the index of the movement in the work
@property NSInteger playedCount;  // number of times this track has been played
@property (copy) NSDate *playedDate;  // the date and time this track was last played
@property (copy, readonly) NSString *purchaserAppleID;  // the Apple ID of the person who purchased this track
@property (copy, readonly) NSString *purchaserName;  // the name of the person who purchased this track
@property NSInteger rating;  // the rating of this track (0 to 100)
@property (readonly) iTunesERtK ratingKind;  // the rating kind of this track
@property (copy, readonly) NSDate *releaseDate;  // the release date of this track
@property (readonly) NSInteger sampleRate;  // the sample rate of the track (in Hz)
@property NSInteger seasonNumber;  // the season number of the track
@property BOOL shufflable;  // is this track included when shuffling?
@property NSInteger skippedCount;  // number of times this track has been skipped
@property (copy) NSDate *skippedDate;  // the date and time this track was last skipped
@property (copy) NSString *show;  // the show name of the track
@property (copy) NSString *sortAlbum;  // override string to use for the track when sorting by album
@property (copy) NSString *sortArtist;  // override string to use for the track when sorting by artist
@property (copy) NSString *sortAlbumArtist;  // override string to use for the track when sorting by album artist
@property (copy) NSString *sortName;  // override string to use for the track when sorting by name
@property (copy) NSString *sortComposer;  // override string to use for the track when sorting by composer
@property (copy) NSString *sortShow;  // override string to use for the track when sorting by show name
@property (readonly) long long size;  // the size of the track (in bytes)
@property double start;  // the start time of the track in seconds
@property (copy, readonly) NSString *time;  // the length of the track in MM:SS format
@property NSInteger trackCount;  // the total number of tracks on the source album
@property NSInteger trackNumber;  // the index of the track on the source album
@property BOOL unplayed;  // is this track unplayed?
@property iTunesEVdK videoKind;  // kind of video track
@property NSInteger volumeAdjustment;  // relative volume adjustment of the track (-100% to 100%)
@property (copy) NSString *work;  // the work name of the track
@property NSInteger year;  // the year the track was recorded/released

@end

// a track on an audio CD
@interface iTunesAudioCDTrack : iTunesTrack

@property (copy, readonly) NSURL *location;  // the location of the file represented by this track

@end

// a track representing an audio file (MP3, AIFF, etc.)
@interface iTunesFileTrack : iTunesTrack

@property (copy) NSURL *location;  // the location of the file represented by this track

- (void) refresh;  // update file track information from the current information in the track’s file

@end

// a track residing in a shared library
@interface iTunesSharedTrack : iTunesTrack

@end

// a track representing a network stream
@interface iTunesURLTrack : iTunesTrack

@property (copy) NSString *address;  // the URL for this track

@end

// custom playlists created by the user
@interface iTunesUserPlaylist : iTunesPlaylist

- (SBElementArray<iTunesFileTrack *> *) fileTracks;
- (SBElementArray<iTunesURLTrack *> *) URLTracks;
- (SBElementArray<iTunesSharedTrack *> *) sharedTracks;

@property BOOL shared;  // is this playlist shared?
@property (readonly) BOOL smart;  // is this a Smart Playlist?
@property (readonly) BOOL genius;  // is this a Genius Playlist?

@end

// a folder that contains other playlists
@interface iTunesFolderPlaylist : iTunesUserPlaylist

@end

// a visual plug-in
@interface iTunesVisual : iTunesItem

@end

// any window
@interface iTunesWindow : iTunesItem

@property NSRect bounds;  // the boundary rectangle for the window
@property (readonly) BOOL closeable;  // does the window have a close button?
@property (readonly) BOOL collapseable;  // does the window have a collapse button?
@property BOOL collapsed;  // is the window collapsed?
@property BOOL fullScreen;  // is the window full screen?
@property NSPoint position;  // the upper left position of the window
@property (readonly) BOOL resizable;  // is the window resizable?
@property BOOL visible;  // is the window visible?
@property (readonly) BOOL zoomable;  // is the window zoomable?
@property BOOL zoomed;  // is the window zoomed?

@end

// the main iTunes window
@interface iTunesBrowserWindow : iTunesWindow

@property (copy, readonly) SBObject *selection;  // the selected songs
@property (copy) iTunesPlaylist *view;  // the playlist currently displayed in the window

@end

// the iTunes equalizer window
@interface iTunesEQWindow : iTunesWindow

@end

// the miniplayer window
@interface iTunesMiniplayerWindow : iTunesWindow

@end

// a sub-window showing a single playlist
@interface iTunesPlaylistWindow : iTunesWindow

@property (copy, readonly) SBObject *selection;  // the selected songs
@property (copy, readonly) iTunesPlaylist *view;  // the playlist displayed in the window

@end

// the video window
@interface iTunesVideoWindow : iTunesWindow

@end


text:
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11762" systemVersion="16C67" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
    <dependencies>
        <deployment identifier="macosx"/>
        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11762"/>
    </dependencies>
    <objects>
        <customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
            <connections>
                <outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
            </connections>
        </customObject>
        <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
        <customObject id="-3" userLabel="Application" customClass="NSObject"/>
        <customObject id="Voe-Tx-rLC" customClass="AppDelegate"/>
        <customObject id="YLy-65-1bz" customClass="NSFontManager"/>
    </objects>
</document>

repo_name: milgra/macmediakeyforwarder

repo_language: Objective-C

mime_type: text/x-objective-c


text:
/*
  Localizable.strings
  MacMediaKeyForwarder

  Created by Sebastiaan Pasma on 2019. 08. 16..
  Copyright © 2017년 Milan Toth. All rights reserved.
*/

"Send events to both players" = "Stuur events naar beide spelers";
"Open at login" = "Openen bij inloggen";
"Prioritize iTunes" = "Prioriteer iTunes";
"Prioritize Spotify" = "Prioriteer Spotify";
"Pause" = "Pauze";
"Pause if no player is running" = "Pauzeren als er geen speler geopend is";
"Donate if you like the app" = "Doneren";
"Check for updates" = "Controleer op updates";
"Quit" = "Afsluiten";
"OK" = "OK";
"Cancel" = "Annuleren";


text:
/*
  Localizable.strings
  MacMediaKeyForwarder

  Created by Sungho Lee on 2017. 12. 27..
  Copyright © 2017년 Milan Toth. All rights reserved.
*/

"Send events to both players" = "Enviar eventos a ambos reproductores";
"Open at login" = "Abrir al iniciar sesión";
"Prioritize iTunes" = "Dar prioridad a iTunes";
"Prioritize Spotify" = "Dar prioridad a Spotify";
"Pause" = "Pausar";
"Pause if no player is running" = "Pausar si ningún reproductor se está ejecutando";
"Donate if you like the app" = "Dona si te gusta la app";
"Check for updates" = "Verificar actualizaciones";
"Quit" = "Salir";


text:
"Send events to both players" = "同時控制 iTunes 及 Spotify";
"Open at login" = "登入時開啟";
"Hide from menu bar" = "從選單列中隱藏";
"Prioritize iTunes" = "iTunes 優先";
"Prioritize Spotify" = "Spotify 優先";
"Pause" = "暫停";
"Pause if no player is running" = "無播放器執行時暫停";
"Donate if you like the app" = "如果喜歡這款應用歡迎贊助";
"Check for updates" = "檢查更新";
"Quit" = "結束";
"OK" = "OK";
"Cancel" = "取消";


text:
/*
  Localizable.strings
  HighSierraMediaKeyEnabler

  Created by Sungho Lee on 2017. 12. 27..
  Copyright © 2017년 Milan Toth. All rights reserved.
*/

"Send events to both players" = "Wyślij zdarzenie do obu odtwarzaczy";
"Open at login" = "Otwórz przy zalogowaniu";
"Prioritize iTunes" = "Priorytetyzuj iTunes";
"Prioritize Spotify" = "Priorytetyzuj Spotify";
"Pause" = "Pauza";
"Pause if no player is running" = "Zapauzuj, jeśli żaden odtwarzacz nie jest uruchomiony";
"Donate if you like the app" = "Wesprzyj, jeśli lubisz tę aplikację";
"Check for updates" = "Sprawdź uaktualnienia";
"Quit" = "Wyjdź";
"OK" = "OK";
"Cancel" = "Anuluj";


text:
"Send events to both players" = "両方のプレーヤーを操作";
"Open at login" = "ログイン時に起動";
"Prioritize iTunes" = "iTunesを優先";
"Prioritize Spotify" = "Spotifyを優先";
"Pause" = "一時動作停止";
"Pause if no player is running" = "プレーヤー未起動時に動作停止";
"Donate if you like the app" = "アプリを気に入ったら寄付";
"Check for updates" = "アップデートをチェック";
"Quit" = "終了";
"OK" = "OK";
"Cancel" = "キャンセル";


text:
/*
  Localizable.strings
  HighSierraMediaKeyEnabler

  Created by Sungho Lee on 2017. 12. 27..
  Copyright © 2017년 Milan Toth. All rights reserved.
*/

"Send events to both players" = "Send handling til begge afspillere";
"Open at login" = "Åbn ved login";
"Prioritize iTunes" = "Prioriter iTunes";
"Prioritize Spotify" = "Prioriter Spotify";
"Pause" = "Pause";
"Pause if no player is running" = "Pause hvis ingen afspiller kører";
"Donate if you like the app" = "Doner hvis du synes om appen";
"Check for updates" = "Søg efter opdateringer";
"Quit" = "Afslut";


text:
/*
  Localizable.strings
  MacMediaKeyForwarder

  Created by Sungho Lee on 2017. 12. 27..
  Copyright © 2017년 Milan Toth. All rights reserved.
*/

"Send events to both players" = "Send events to both players";
"Open at login" = "Open at login";
"Prioritize iTunes" = "Prioritize iTunes";
"Prioritize Spotify" = "Prioritize Spotify";
"Pause" = "Pause";
"Pause if no player is running" = "Pause if no player is running";
"Donate if you like the app" = "Donate if you like the app";
"Check for updates" = "Check for updates";
"Quit" = "Quit";
"OK" = "OK";
"Cancel" = "Cancel";


text:
/*
  Localizable.strings
  HighSierraMediaKeyEnabler

  Created by e1ectron on 2018. 02. 12..
  Copyright © 2018년 Milan Toth. All rights reserved.
*/

"Send events to both players" = "Отправлять события на оба плеера";
"Prioritize iTunes" = "Предпочитать iTunes";
"Prioritize Spotify" = "Предпочитать Spotify";
"Pause" = "Пауза";
"Pause if no player is running" = "Пауза, если ни один плеер не запущен";
"Donate if you like the app" = "Поддержать разработку";
"Check for updates" = "Проверить обновления";
"Quit" = "Завершить";


text:
/*
  Localizable.strings
  MacMediaKeyForwarder

  Created by Sungho Lee on 2017. 12. 27..
  Copyright © 2017년 Milan Toth. All rights reserved.
*/

"Send events to both players" = "Ohjaa molempia soittimia";
"Open at login" = "Avaa sisäänkirjautuessa";
"Hide from menu bar" = "Piilota valikkoriviltä";
"Prioritize iTunes" = "iTunes etusijalla";
"Prioritize Spotify" = "Spotify etusijalla";
"Pause" = "Tauko";
"Pause if no player is running" = "Tauko soittimien ollessa kiinni";
"Donate if you like the app" = "Lahjoita, jos pidät sovelluksesta";
"Check for updates" = "Tarkista päivitykset";
"Quit" = "Sulje";
"OK" = "OK";
"Cancel" = "Peruuta";


text:
/*
  Localizable.strings
  MacMediaKeyForwarder

  Created by Sungho Lee on 2017. 12. 27..
  Copyright © 2017년 Milan Toth. All rights reserved.
*/

"Send events to both players" = "iTunes, Spotify에 모두 이벤트 보내기";
"Open at login" = "부팅시 자동 실행";
"Prioritize iTunes" = "iTunes에 우선권 부여";
"Prioritize Spotify" = "Spotify에 우선권 부여";
"Pause" = "일시정지 (macOS 기본값 사용)";
"Pause if no player is running" = "iTunes 또는 Spotify가 열려있지 않으면 일시정지";
"Donate if you like the app" = "이 앱이 마음에 들면 기부해주세요!";
"Check for updates" = "업데이트 확인";
"Quit" = "종료";


text:
/*
  Localizable.strings
  HighSierraMediaKeyEnabler

  Created by Sungho Lee on 2017. 12. 27..
  Copyright © 2017년 Milan Toth. All rights reserved.
*/

"Send events to both players" = "Események küldése mindkét lejátszónak";
"Open at login" = "Automatikus megnyitás bejelentkezéskor";
"Prioritize iTunes" = "Az iTunes fontosabb";
"Prioritize Spotify" = "A Spotify fontosabb";
"Pause" = "Szünet";
"Pause if no player is running" = "Szüneteltetés ha nem fut lejátszó";
"Donate if you like the app" = "Adományozz ha tetszik az app";
"Check for updates" = "Frissítések keresése";
"Quit" = "Kilépés";


text:
/*
  Localizable.strings
  MacMediaKeyForwarder

  Created by maciboy on 2018. 02. 03..
  Copyright © 2018년 Milan Toth. All rights reserved.
*/

"Send events to both players" = "Eingaben an beide Player senden";
"Open at login" = "Beim Start öffnen";
"Prioritize iTunes" = "iTunes priorisieren";
"Prioritize Spotify" = "Spotify priorisieren";
"Pause" = "Pausieren";
"Pause if no player is running" = "Pausieren, wenn kein Player geöffnet ist";
"Donate if you like the app" = "Spenden";
"Check for updates" = "Nach Updates suchen";
"Quit" = "Beenden";


text:
"Send events to both players" = "Envoyer les événements aux deux lecteurs";
"Open at login" = "Lancer au démarrage";
"Hide from menu bar" = "Masquer l'icône dans la barre";
"Prioritize iTunes" = "Privilégier iTunes";
"Prioritize Spotify" = "Privilégier Spotify";
"Pause" = "Pause";
"Pause if no player is running" = "Mettre en pause si aucun lecteur n'est ouvert";
"Donate if you like the app" = "Faire un don si vous aimez l'application";
"Check for updates" = "Vérifier les mises à jour";
"Quit" = "Quitter";
"OK" = "OK";
"Cancel" = "Annuler";


text:
//
//  GBLaunchAtLogin.m
//  GBLaunchAtLogin
//
//  Created by Luka Mirosevic on 04/03/2013.
//  Copyright (c) 2013 Goonbee. All rights reserved.
//
//  Credit where credit is due, most of this code is borrowed from somewhere and is just being wrapped into a convenient ObjC library here. I don't remember the original source so if you are, or know, the author please let me know.

#import "GBLaunchAtLogin.h"

@implementation GBLaunchAtLogin

+(BOOL)isLoginItem {
	NSString *appPath = [[NSBundle mainBundle] bundlePath];

	// This will retrieve the path for the application
	// For example, /Applications/test.app
	CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:appPath];

	// Create a reference to the shared file list.
	LSSharedFileListRef loginItems = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);

	if (loginItems) {
		UInt32 seedValue;
		//Retrieve the list of Login Items and cast them to a NSArray so that it will be easier to iterate.
		NSArray  *loginItemsArray = (NSArray *)CFBridgingRelease(LSSharedFileListCopySnapshot(loginItems, &seedValue));
		for (int i=0 ; i<[loginItemsArray count]; i++) {
			LSSharedFileListItemRef itemRef = (__bridge LSSharedFileListItemRef)loginItemsArray[i];
			//Resolve the item with URL
			if (LSSharedFileListItemResolve(itemRef, 0, (CFURLRef *)&url, NULL) == noErr) {
				NSString *urlPath = [(__bridge NSURL *)url path];
				if ([urlPath compare:appPath] == NSOrderedSame) {
					return YES;
				}
			}
		}
	}

    return NO;
}

+(void)addAppAsLoginItem {
	NSString *appPath = [[NSBundle mainBundle] bundlePath];

	// This will retrieve the path for the application
	// For example, /Applications/test.app
	CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:appPath];

	// Create a reference to the shared file list.
    // We are adding it to the current user only.
    // If we want to add it all users, use
    // kLSSharedFileListGlobalLoginItems instead of
    //kLSSharedFileListSessionLoginItems
	LSSharedFileListRef loginItems = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
	if (loginItems) {
		//Insert an item to the list.
		LSSharedFileListItemRef item = LSSharedFileListInsertItemURL(loginItems, kLSSharedFileListItemLast, NULL, NULL, url, NULL, NULL);
		if (item) {
			CFRelease(item);
        }
	}

	CFRelease(loginItems);
}

+(void)removeAppFromLoginItems {
	NSString *appPath = [[NSBundle mainBundle] bundlePath];

	// This will retrieve the path for the application
	// For example, /Applications/test.app
	CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:appPath];

	// Create a reference to the shared file list.
	LSSharedFileListRef loginItems = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);

	if (loginItems) {
		UInt32 seedValue;
		//Retrieve the list of Login Items and cast them to
		// a NSArray so that it will be easier to iterate.
		NSArray  *loginItemsArray = (NSArray *)CFBridgingRelease(LSSharedFileListCopySnapshot(loginItems, &seedValue));
		for (int i=0 ; i<[loginItemsArray count]; i++) {
			LSSharedFileListItemRef itemRef = (__bridge LSSharedFileListItemRef)loginItemsArray[i];
			//Resolve the item with URL
			if (LSSharedFileListItemResolve(itemRef, 0, (CFURLRef *)&url, NULL) == noErr) {
				NSString *urlPath = [(__bridge NSURL *)url path];
				if ([urlPath compare:appPath] == NSOrderedSame) {
					LSSharedFileListItemRemove(loginItems, itemRef);
				}
			}
		}
	}
}

@end


text:
//
//  GBLaunchAtLogin.h
//  GBLaunchAtLogin
//
//  Created by Luka Mirosevic on 04/03/2013.
//  Copyright (c) 2013 Goonbee. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface GBLaunchAtLogin : NSObject

+(BOOL)isLoginItem;
+(void)addAppAsLoginItem;
+(void)removeAppFromLoginItems;

@end


text:
GBLaunchAtLogin
============

Add your app as a login item on Mac OS X.

Usage
------------

Make your app launch at login:

```objective-c
[GBLaunchAtLogin addAppAsLoginItem];
```

Check if the app has been added as a login item:

```objective-c
[GBLaunchAtLogin isLoginItem];		//Returns: YES or NO
```

Remove app from login items:

```objective-c
[GBLaunchAtLogin removeAppFromLoginItems];
```

Don't forget to import header first.

```objective-c
#import <GBLaunchAtLogin/GBLaunchAtLogin.h>
```

Integration
------------

1. Add GBLaunchAtLogin as a subproject (by dragging the .xcodeproj file from Finder into the Xcode Project Navigator, or using the "Add Files" menu)
2. Add project dependency to your target (in "Target Dependencies" under "Build Phases")
3. Link library to your target (in "Link Binary With Libraries" under "Build Phases")

Copyright & License
------------

Copyright 2013 Luka Mirosevic

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License in the LICENSE file, or at:

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/lmirosevic/gblaunchatlogin/trend.png)](https://bitdeli.com/free "Bitdeli Badge")


text:
%% start of file `template.tex'.
%% Copyright 2006-2015 Xavier Danaux (xdanaux@gmail.com).
%
% This work may be distributed and/or modified under the
% conditions of the LaTeX Project Public License version 1.3c,
% available at http://www.latex-project.org/lppl/.

\documentclass[10pt,a4paper,sans]{moderncv}        % possible options include font size ('10pt', '11pt' and '12pt'), paper size ('a4paper', 'letterpaper', 'a5paper', 'legalpaper', 'executivepaper' and 'landscape') and font family ('sans' and 'roman')

% moderncv themes
\moderncvstyle{classic}                             % style options are 'casual' (default), 'classic', 'banking', 'oldstyle' and 'fancy'
\moderncvcolor{blue}                               % color options 'black', 'blue' (default), 'burgundy', 'green', 'grey', 'orange', 'purple' and 'red'
%\renewcommand{\familydefault}{\sfdefault}         % to set the default font; use '\sfdefault' for the default sans serif font, '\rmdefault' for the default roman one, or any tex font name
\nopagenumbers{}                                  % uncomment to suppress automatic page numbering for CVs longer than one page

% character encoding
%\usepackage[utf8]{inputenc}                       % if you are not using xelatex ou lualatex, replace by the encoding you are using
%\usepackage{CJKutf8}                              % if you need to use CJK to typeset your resume in Chinese, Japanese or Korean

% adjust the page margins
\usepackage[scale=0.75]{geometry}
%\setlength{\hintscolumnwidth}{3cm}                % if you want to change the width of the column with the dates
%\setlength{\makecvheadnamewidth}{10cm}            % for the 'classic' style, if you want to force the width allocated to your name and avoid line breaks. be careful though, the length is normally calculated to avoid any overlap with your personal info; use this at your own typographical risks...

% personal data
\name{Marisa}{Kirisame}
\email{marisa@cs.utah.edu}                               % optional, remove / comment the line if not wanted
\homepage{www.marisa.moe}                         % optional, remove / comment the line if not wanted
\social[github]{https://github.com/MarisaKirisame/}                              % optional, remove / comment the line if not wanted

% bibliography adjustements (only useful if you make citations in your resume, or print a list of publications using BibTeX)
%   to show numerical labels in the bibliography (default is to show no labels)
%\makeatletter\renewcommand*{\bibliographyitemlabel}{\@biblabel{\arabic{enumiv}}}\makeatother
\renewcommand*{\bibliographyitemlabel}{[\arabic{enumiv}]}
%   to redefine the bibliography heading string ("Publications")
%\renewcommand{\refname}{Articles}

% bibliography with mutiple entries
%\usepackage{multibib}
%\newcites{book,misc}{{Books},{Others}}
%----------------------------------------------------------------------------------
%            content
%----------------------------------------------------------------------------------
\begin{document}
%\begin{CJK*}{UTF8}{gbsn}                          % to typeset your resume in Chinese using CJK
%-----       resume       ---------------------------------------------------------
\makecvtitle

\vspace{-.5in}
\section{Research}
\cvitem{DTR}{Developed an algorithm for gradient checkpointing for large machine learning model. Currently upstreaming to Pytorch. Adopted by Megengine, DELTA, and used in production.}
\cvitem{MemBalancer}{Worked at controlling the garbage collector for V8, the Javascript engine behind Chrome. Utilize concurrent programming and garbage collection knowledge.}
\cvitem{TVM}{Top 20 contributor to high performance ML compiler-runtime. Contributed to the design of Relay, a higher order, differentiable IR. Implemented Algebraic Data Types, Automatic Differentiation, Reference, Pretty Printing, Ahead-Of-Time Compiler, Partial Evaluator, contributed to Type Inference.}

\section{Education}
\cventry{2020--}{PhD in CS}{University of Utah}{Salt Lake City}{}{} % arguments 3 to 6 can be left empty
\cventry{2019--2020}{Master in CS}{University of Washington}{Seattle}{}{} % arguments 3 to 6 can be left empty
\cventry{2015--2019}{Bachelor in CS}{University of Washington}{Seattle}{}{} % arguments 3 to 6 can be left empty

% Publications from a BibTeX file without multibib
%  for numerical labels: \renewcommand{\bibliographyitemlabel}{\@biblabel{\arabic{enumiv}}}% CONSIDER MERGING WITH PREAMBLE PART
%  to redefine the heading string ("Publications"): \renewcommand{\refname}{Articles}

\nocite{*}
\bibliographystyle{unsrt}
\bibliography{publications}                        % 'publications' is the name of a BibTeX file

\section{Projects}
\cvitem{7Tree}{Using CEGIS and Ltac's logical programming capability, build a push-button program synthesizer and verifier for a domain specific problem in Coq.}
\cvitem{Happy-Tree}{A polytypic decision tree in Haskell that work on any True-Sums-Of-Products.}
\cvitem{Ordinary}{A small web game to teach programming. Used Functional Reactive Programming, Nix, Zipper, and GHCJS.}
\cvitem{PE}{Simply Typed Lambda Calculus with reference/product/sum with Bidirectional Type Checking, Partial Evaluation, Automatic Differentiation. Written in MetaOCaml so it can be compiled to OCaml.}
\cvitem{Prover}{An automated theorem prover for first order logic that use Gentzen's Sequent Calculus. Logic Formula represented as Generalized Algebraic Data Type using TMP in C++.}
\cvitem{AI}{Implemented multiple search algorithms in AI Modern Approach, Including A Star, Bidirectional Breath First Search, Constraint Satisfication Programming with K Arch Consistency optimization. Heavily used Iterator Style and Boost to increase efficiency.}
\cvitem{Language}{Fluent in Mandarin, Cantonese, and English.}
\section{Coursework}
\cvlistdoubleitem{Programming Languages, Deep Learning}{Operating Systems}
\cvlistdoubleitem{Advanced Computer Architecture}{Database}
\cvlistdoubleitem{Graduate Theoretical Computer Science}{Systems for Machine Learning}

% Publications from a BibTeX file using the multibib package
%\section{Publications}
%\nocitebook{book1,book2}
%\bibliographystylebook{plain}
%\bibliographybook{publications}                   % 'publications' is the name of a BibTeX file
%\nocitemisc{misc1,misc2,misc3}
%\bibliographystylemisc{plain}
%\bibliographymisc{publications}                   % 'publications' is the name of a BibTeX file

\clearpage
\end{document}

%% end of file `template.tex'.

repo_name: MarisaKirisame/resume

repo_language: TeX

mime_type: text/plain


text:
@misc{https://doi.org/10.48550/arxiv.2204.10455,
  doi = {10.48550/ARXIV.2204.10455},

  url = {https://arxiv.org/abs/2204.10455},

  author = {Kirisame, Marisa and Shenoy, Pranav and Panchekha, Pavel},

  keywords = {Programming Languages (cs.PL), Systems and Control (eess.SY), FOS: Computer and information sciences, FOS: Computer and information sciences, FOS: Electrical engineering, electronic engineering, information engineering, FOS: Electrical engineering, electronic engineering, information engineering},

  title = {Optimal Heap Limits for Reducing Browser Memory Use},

  publisher = {arXiv},

  year = {2022},

  copyright = {arXiv.org perpetual, non-exclusive license}
}

@inproceedings{kirisame2021dynamic,
  title = {Dynamic Tensor Rematerialization},
  author = {Marisa Kirisame and
            Steven Lyubomirsky and
            Altan Haan and
            Jennifer Brennan and
            Mike He and
            Jared Roesch and
            Tianqi Chen and
            Zachary Tatlock},
  booktitle = {International Conference on Learning Representations},
  year = {2021},
  url = {https://openreview.net/forum?id=Vfs_2RnOD0H}
}

@article{Relay,
  author    = {Jared Roesch and
               Steven Lyubomirsky and
               Marisa Kirisame and
               Josh Pollock and
               Logan Weber and
               Ziheng Jiang and
               Tianqi Chen and
               Thierry Moreau and
               Zachary Tatlock},
  title     = {Relay: {A} High-Level {IR} for Deep Learning},
  journal   = {CoRR},
  volume    = {abs/1904.08368},
  year      = {2019},
  url       = {http://arxiv.org/abs/1904.08368},
  archivePrefix = {arXiv},
  eprint    = {1904.08368},
  timestamp = {Fri, 26 Apr 2019 13:18:53 +0200},
  biburl    = {https://dblp.org/rec/journals/corr/abs-1904-08368.bib},
  bibsource = {dblp computer science bibliography, https://dblp.org}
}


text:
[metadata]
description-file = README.md

repo_language: Python


text:
python-bidi>=0.4
google-api-python-client>=2.66
google-auth-httplib2>=0.1.
google-auth-oauthlib>=0.7
aiohttp>=3.8
typing_extensions


text:
import setuptools

with open("README.md", "r") as fh:
    long_description = fh.read()

setuptools.setup(
    name="i3-agenda",
    version="1.7",
    author="Tomer Rosenfeld",
    author_email="mail@tomerrosenfeld.com",
    description="Show your next google calendar event in polybar or i3-bar",
    long_description=long_description,
    long_description_content_type="text/markdown",
    url="https://github.com/rosenpin/i3-agenda",
    download_url="https://github.com/rosenpin/i3-agenda/archive/1.7.tar.gz",
    packages=setuptools.find_packages(),
    license="Unlicense",
    classifiers=["Programming Language :: Python :: 3"],
    install_requires=[
        "python-bidi",
        "google-api-python-client",
        "google-auth-httplib2",
        "google-auth-oauthlib",
        "aiohttp",
    ],
    scripts=[
        "i3_agenda/API.py",
        "i3_agenda/cache_utils.py",
        "i3_agenda/config.py",
        "i3_agenda/const.py",
        "i3_agenda/event.py",
        "i3_agenda/helpers.py",
        "i3_agenda/i3_agenda.py",
    ],
    entry_points={
        "console_scripts": [
            "i3-agenda = i3_agenda:main",
        ],
    },
    python_requires=">=3.3",
)


text:
[![AUR version](https://img.shields.io/aur/version/i3-agenda?style=flat-square&logo=arch-linux)](https://aur.archlinux.org/packages/i3-agenda/)
[![PyPI](https://img.shields.io/pypi/v/i3-agenda?style=flat-square&logo=python)](https://pypi.org/project/i3-agenda/)

# What is this?
It's a script that communicates with Google's calendar API, it will go through your calendars and print the next calendar event details.\
You can take this output and show it on your i3-bar or polybar

![example](https://raw.githubusercontent.com/rosenpin/i3-agenda/master/art/screenshot.png)
![example](https://raw.githubusercontent.com/rosenpin/i3-agenda/master/art/mac_screenshot.png)

## How does it work
It will read your next 10 events from each of your calendars, then go through them all and figure out which one is closest.\
It will print the time and title of the closest event.

# Setup

## Google API
https://developers.google.com/calendar/quickstart/python

1. You need to create a Google API project and download your OAuth 2.0 credentials json file.\
You first need to create a project [here](https://console.developers.google.com/apis/credentials), then add Google Calendar support, then download the credentials.json file.\
**Alternatively, you can just use [this link](https://developers.google.com/calendar/quickstart/python) and click "Enable the Google Calendar API". This will create a project, add Google Calendar support, and let you download the file in 1 click**.\
If you're having trouble, you can use this tutorial for more information [https://developers.google.com/calendar/auth](https://developers.google.com/calendar/auth).\
Another great guide can be found here if you're still having trouble: [https://github.com/jay0lee/GAM/wiki/CreatingClientSecretsFile](https://github.com/jay0lee/GAM/wiki/CreatingClientSecretsFile).
2. Download the credentials file to somewhere on your computer.
3. Proceed to installation phase.

## Installation
After downloading the credentials file, install the package.

### Pip
1. `sudo pip install i3-agenda`
2. Try running `i3-agenda -c $CREDENTIALS_FILE_PATH` with "$CREDENTIALS_FILE_PATH" replaced with the path to the credentials.json file you downloaded in the previous step.
3. Add configuration to your bar (examples in the Examples section below).

### Arch Linux (AUR)
1. `yay -S i3-agenda-git`
2. Try running `i3-agenda -c $CREDENTIALS_FILE_PATH` with "$CREDENTIALS_FILE_PATH" replaced with the path to the credentials.json file you downloaded in the previous step.
3. Add configuration to your bar (examples in the Examples section below).

### Manual
#### Dependencies
You need to install some python libraries first.\
Make sure python3 is your default python.\
Run `sudo pip3 install python-bidi google-api-python-client google-auth-httplib2 google-auth-oauthlib`

1. Clone the repo to a local directory `cd ~/ && git clone https://github.com/rosenpin/i3-agenda && cd i3-agenda`
3. Run the script `python3 i3_agenda/i3_agenda.py -c $CREDENTIALS_FILE_PATH` with "$CREDENTIALS_FILE_PATH" replaced with the path to the credentials.json file you downloaded in the previous step. If configured correctly, it will prompt you to log in in your browser, accept everything. It should print your next event.
4. Optional: you can run `sudo python setup.py install` to add the script to your path so you can run `i3-agenda` anywhere.
5. Add configuration to your bar (examples in the Examples section below).

# Usage
```
  -h, --help            show this help message and exit
  --credentials CREDENTIALS, -c CREDENTIALS
                        path to your credentials.json file
  --conf CONF, -cd CONF
                        path to the i3agenda configuration and cache folder
  --cachettl CACHETTL, -ttl CACHETTL
                        time for cache to be kept in minutes
  --update, -u          when using this flag it will not load previous results from cache, it will however save new results to cache.
                        You can use this flag to refresh all the cache forcefully
  --ids IDS [IDS ...], -i IDS [IDS ...]
                        list of calendar ids to fetch, space separated. If  none is specified all calendars will be fetched
  --maxres MAXRES, -r MAXRES
                        max number of events to query Google's API for each of your calendars.
                        Increase this number if you have lot of events in your google calendar
  --today, -d           print only today events
  --no-event-text TEXT  text to display when there are no events
  --hide-event-after HIDE_EVENT_AFTER
                        minutes to show events after they start before showing the next event.
                        If not specified, the current event will be shown until it ends
  --show-event-before SHOW_EVENT_BEFORE
                        minutes to show events before they start. If not specified, the next event will be shown regardless of when it starts
  --date-format DATE_FORMAT
                        the date format like %d/%m/%y. Default is %d/%m
  --limchar LIMCHAR, -l LIMCHAR
                        the max characters that the displayed event can contain
  --skip SKIP, -s SKIP  the number of events to skip from the most recent
```

### Filter displayed calendars
To display events only from certain calendars use the `--ids` parameter and pass a list of calendar id, space separated.\
To obtain the calendar id you can check the settings page of the calendar on Google (usually is the owner email, if it's not shared).\
Leaving the list empty will fetch all calendars (default behavior).

## Notes
### Known issues
It might not work properly if you have more than 10 all day events, this can be fixed by increasing the maxResults variable.

### RTL support
If you use RTL or some of your events contain RTL languages, you will need to pipe [pybidi](https://pypi.org/project/python-bidi/) with the script. Example:
`i3-agenda -c ~/.google_credentials.json -ttl 60 | pybidi`

### Caching
It uses a caching mechanism so you won't have to contact Google servers every minute, to set the cache TTL use the -ttl flag.\
Example: `i3-agenda --ttl 60` to set the TTL to 60 (meaning it will contact Google again every hour).\
This means that if you create a new event, it might take an hour for the script to recognize it.

### Multi account support
Multi account support is not officialy supported, but you can use the workaround from this issue: https://github.com/rosenpin/i3-agenda/issues/35#issuecomment-923976482

## Examples
### Example [polybar](https://github.com/polybar/polybar) configuration:
![example](https://raw.githubusercontent.com/rosenpin/i3-agenda/master/art/screenshot.png)
``` ini
modules-center = agenda
....
[module/agenda]
type = custom/script
; Show the next event and forget cache automatically every 60 minutes
exec = i3-agenda -c ~/.google_credentials.json -ttl 60
; left click to launch Google Calendar
click-left = chromium https://calendar.google.com/calendar/r/day
; right click force update the cache, if for example you just added a new event
click-right = notify-send "syncing..." && i3-agenda -c ~/.google_credentials.json --update && notify-send -t 2000 "sync finished"
interval = 60
```

### Example [SwiftBar](https://github.com/swiftbar/SwiftBar) configuration
![example](https://raw.githubusercontent.com/rosenpin/i3-agenda/master/art/mac_screenshot.png)
This will show your next event as the menu bar title, when you press it you will see a dropdown with all your today events
You can call the file `agenda.2m.sh` to make it refresh every 2 minutes
``` bash
#!/bin/bash
# <swiftbar.hideAbout>true</swiftbar.hideAbout>

i3-agenda -c ~/.google_credentials.json -ttl 60 --limchar 30

echo "---"
href="href='https://calendar.google.com/calendar/u/0/r/'"

i=1
while :; do
    event=$(i3-agenda -c ~/.google_credentials.json -ttl 60 --limchar 30 --skip $i --today)
    ((i++))
    if [[ "$event" == "No events" ]];then
        exit 0
    fi
    echo "$event | $href"
done
```

### Example i3block configuration:
```ini
[i3-agenda]
command=i3-agenda -c ~/.google_credentials.json -ttl 60
interval=60
```

Example output of the script:\
```10:55 Grocery shopping```

### How to use the `skip` flag to scroll events

Edit the polybar configuration creating two modules:

```ini
[module/agenda-ipc]
type = custom/ipc

hook-0 = i3-agenda -c ~/.google_credentials.json --skip $(cat ~/.config/i3-agenda/i3-agenda-skip.tmp || echo 0)
hook-1 = ~/.config/polybar/scripts/i3agenda-onscroll.sh down && i3-agenda -c ~/.google_credentials.json --skip $(cat ~/.config/i3-agenda/i3-agenda-skip.tmp || echo 0)
hook-2 = ~/.config/polybar/scripts/i3agenda-onscroll.sh up && i3-agenda -c ~/.google_credentials.json --skip $(cat ~/.config/i3-agenda/i3-agenda-skip.tmp || echo 0)

format = %{F#61afef}%{F-} <output>

; left click to launch Google Calendar
click-left = firefox https://calendar.google.com/calendar/u/0/r
; right click force update the cache, if for example you just added a new event
click-right = rm ~/.config/i3-agenda/i3-agenda-skip.tmp; i3-agenda -c ~/.config/i3-agenda/client_secret.json --update && notify-send "i3-agenda" "Sync completed"

; show the previous event
scroll-down = polybar-msg hook agenda-ipc 2
; show the next event
scroll-up = polybar-msg hook agenda-ipc 3

[module/agenda]
type = custom/script
interval = 900
exec = polybar-msg hook agenda-ipc 1
label =
```

Add both modules to the bar, for example:

```ini
modules-center = agenda agenda-ipc
```

In the polybar scripts folder add the file `i3agenda-onscroll.sh`:

```bash
#!/usr/bin/env bash
if [ -n "${1}" ]; then
  file=~/.config/i3-agenda/i3-agenda-skip.tmp
  typeset -i skip=$(cat $file || echo 0)
  if [[ "${1}" == "up" ]]; then
    skip+=1
  elif [[ "${1}" == "down" && $skip -gt 0 ]]; then
    skip=$(( skip - 1))
  fi
  echo $skip > $file
fi
```


text:
# These are supported funding model platforms

github: [rosenpin]
custom: ["https://www.paypal.com/donate/?hosted_button_id=9M8LL9GJXELBN"]


text:
name: Comment on PR
on:
  workflow_run:
    workflows:
      - "Python package"
    types:
      - completed
jobs:
  comment:
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    steps:
      - uses: jwalton/gh-find-current-pr@7ada613939e2a233c83a1320679446fa1c6bdcb9 #v1.3.2
        id: finder
      - name: on artifact
        id: artifact
        uses: marocchino/on_artifact@fa52c295bf1972c5d09a615f31edb68c4438fb06 # v1
        with:
          name: coverage
      - uses: marocchino/sticky-pull-request-comment@fcf6fe9e4a0409cd9316a5011435be0f3327f1e1 # v2.3.1
        with:
          number: ${{ steps.finder.outputs.pr }}
          header: Coverage
          message: |
            ${{ steps.artifact.outputs.code_coverage_md }}


text:
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.

name: Upload Python Package

on:
  release:
    types: [published]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.10'
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install build
      - name: Build package
        run: python -m build
      - name: Publish package
        uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
        with:
          user: __token__
          password: ${{ secrets.PYPI_API_TOKEN }}


text:
name: Python package

on: [push, pull_request]

jobs:
  build:

    runs-on: ubuntu-latest
    # run on push and run on PR if the PR is from a forked repository"
    if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
    strategy:
      matrix:
        python-version: ["3.7", "3.8", "3.9", "3.10"]

    steps:
      - uses: actions/checkout@v3
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install flake8 pytest freezegun pytest-cov
          if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
      - name: Lint with flake8
        run: |
          # stop the build if there are Python syntax errors or undefined names
          flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
          # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
          flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
      - name: Test with pytest
        working-directory: ./i3_agenda
        run: |
          pytest --junitxml=../junit/test-results.xml
          pytest --doctest-modules --cov=. --cov-report=xml:../coverage/cov.xml --cov-report=html:../coverage/

      - name: Code Coverage Report
        uses: irongut/CodeCoverageSummary@51cc3a756ddcd398d447c044c02cb6aa83fdae95
        with:
          filename: coverage/cov.xml
          badge: true
          fail_below_min: true
          format: markdown
          hide_branch_rate: false
          hide_complexity: false
          indicators: true
          output: both
          thresholds: '60 80'
      - name: Save PR number
        run: |
          mkdir -p ./pr
          # this file name `number` will be output variable name later
          echo ${{ github.event.number }} | tee ./pr/number
          cat code-coverage-results.md | tee ./pr/code_coverage_md
      - uses: actions/upload-artifact@v3
        if: ${{ matrix.python-version == '3.10' }}
        with:
          name: coverage
          path: pr/


text:
import os
from os.path import expanduser
import argparse
from const import *

CONF_DIR = expanduser("~") + os.path.sep + ".i3agenda"
URL_REGEX = "(((http|https)://)?[a-zA-Z0-9./?:@\\-_=#]+\\.([a-zA-Z]){2,6}([a-zA-Z0-9.&/?:@\\-_=#])*)"

# i3blocks use this envvar to check the click
button = os.getenv("BLOCK_BUTTON", "")

parser = argparse.ArgumentParser(description="Show next Google Calendar event")
parser.add_argument(
    "--credentials",
    "-c",
    type=str,
    default="",
    help="path to your credentials.json file",
)
parser.add_argument(
    "--conf",
    "-cd",
    type=str,
    default=CONF_DIR,
    help="path to the i3agenda configuration and cache folder",
)
parser.add_argument(
    "--cachettl",
    "-ttl",
    type=int,
    default=30,
    help="time for cache to be kept in minutes",
)
parser.add_argument(
    "--update",
    "-u",
    action="store_true",
    default=False,
    help="""when using this flag it will not load previous results from cache, it will however save
            new results to cache. You can use this flag to refresh all the cache forcefully""",
)
parser.add_argument(
    "--ids",
    "-i",
    type=str,
    default=[],
    nargs="+",
    help="list of calendar ids to fetch, space separated. If none is specified all calendars will be fetched",
)
parser.add_argument(
    "--maxres",
    "-r",
    type=int,
    default=10,
    help="""max number of events to query Google's API for each of your calendars. Increase this number if you
            have lot of events in your google calendar""",
)
parser.add_argument(
    "--today", "-d", action="store_true", help="print only today events"
)
parser.add_argument(
    "--no-event-text",
    default="No events",
    metavar="TEXT",
    help="text to display when there are no events",
)
parser.add_argument(
    "--hide-event-after",
    type=int,
    default=MIN_DELAY,
    help="""minutes to show events after they start before showing the next event. If not specified, the current event
            will be shown until it ends""",
)
parser.add_argument(
    "--show-event-before",
    type=int,
    default=MIN_DELAY,
    help="""minutes to show events before they start. If not specified, the next event
            will be shown regardless of when it starts""",
)
parser.add_argument(
    "--date-format",
    type=str,
    default="%d/%m",
    help="the date format like %%d/%%m/%%y used for events starting in more than on week. Default is %%d/%%m",
)
parser.add_argument(
    "--limchar",
    "-l",
    type=int,
    default=MIN_CHARS,
    help="the max characters that the displayed event can contain",
)
parser.add_argument(
    "--skip",
    "-s",
    type=int,
    default=0,
    help="the number of events to skip from the most recent",
)
parser.add_argument(
    "--ongoing-time-left",
    "-o",
    action="store_true",
    help="print time left instead of the end time for ongoing events (22m left) instead of (ends 12:00)",
)
parser.add_argument(
    "--next-event-time-left",
    "-n",
    action="store_true",
    help="print the time remaining before the next event starts (event_name in 22m) instead of (12:00 event_name)",
)


text:
import datetime as dt
import json
import re
import time
from typing import List, Optional, Any, Union, Dict

from bidi.algorithm import get_display

from config import MIN_CHARS, MIN_DELAY, URL_REGEX
from const import *
from helpers import get_unix_time, human_delta
from dataclasses import dataclass

@dataclass
class Event:
    summary: str
    start_time: int
    end_time: int
    location: Union[str, None]

    def get_datetime(self) -> dt.datetime:
        return dt.datetime.fromtimestamp(self.start_time)

    def get_end_datetime(self) -> dt.datetime:
        return dt.datetime.fromtimestamp(self.end_time)

    def get_string(
        self,
        limit_char: int,
        date_format: str,
        ongoing_time_left: bool = False,
        next_event_time_left: bool = False,
    ) -> str:
        event_datetime = self.get_datetime()

        result = self.summary
        trimmed = ""
        if MIN_CHARS < limit_char < len(result):
            trimmed = "".join([c for c in result][:limit_char])

        # this is done to preserve RTL while adding the "..." since the get_display is applied after adding the "..."
        if trimmed:
            result = trimmed + "..."
        result = str(get_display(result))

        if self.is_ongoing():
            if ongoing_time_left:
                time_left = self.get_end_datetime() - dt.datetime.now()
                return f"{result} ({human_delta(time_left)} left)"
            else:
                return f"{result} (ends {self.get_end_datetime():%H:%M})"
        elif self.is_today():
            if not self.is_ongoing() and next_event_time_left:
                time_left = event_datetime - dt.datetime.now()
                return f"{result} in {human_delta(time_left)}"
            else:
                return f"{event_datetime:%H:%M} {result}"
        elif self.is_tomorrow():
            return f"{event_datetime:Tomorrow at %H:%M} {result}"
        elif self.is_this_week():
            return f"{event_datetime:%a at %H:%M} {result}"
        else:
            return f"{event_datetime:{date_format} at %H:%M} {result}"

    def is_ongoing(self) -> bool:
        now = dt.datetime.now()
        ongoing = now > self.get_datetime() and not now > self.get_end_datetime()
        return ongoing

    def is_today(self) -> bool:
        today = dt.datetime.today()
        return self.get_datetime().date() == today.date()

    def is_tomorrow(self) -> bool:
        tomorrow = dt.datetime.today() + dt.timedelta(days=1)
        return self.get_datetime().date() == tomorrow.date()

    def is_this_week(self) -> bool:
        today = dt.datetime.today()
        next_week = today + dt.timedelta(days=DAYS_PER_WEEK)
        return today.date() <= self.get_datetime().date() < next_week.date()

    def is_urgent(self) -> bool:
        now = dt.datetime.now()
        urgent = now + dt.timedelta(minutes=URGENT_DELAY_MN)
        five_minutes_started = self.get_datetime() + dt.timedelta(minutes=URGENT_DELAY_MN)
        # is urgent if it begins in URGENT_DELAY_MN minutes and if it hasn't
        # passed URGENT_DELAY_MN minutes it started
        return self.get_datetime() < urgent and not now > five_minutes_started

    def is_allday(self) -> bool:
        time_delta = self.end_time - self.start_time
        # event is considered all day if its start time and end time are both 00:00:00
        # and the time difference between start and finish is divisible by 24
        return self.get_datetime().time() == dt.time(0) \
                and self.get_end_datetime().time() == dt.time(0) \
                and time_delta % SECONDS_PER_DAY == 0

class EventEncoder(json.JSONEncoder):
    def default(self, o):  # pylint: disable=E0202
        if isinstance(o, Event):
            return o.__dict__
        else:
            return json.JSONEncoder.default(self, o)

def sort_events(events: List[Event]) -> List[Event]:
    return sorted(events, key=lambda e: e.start_time, reverse=False)

def get_future_events(events: List[Event], hide_event_after: int, show_event_before: int) -> List[Event]:
    future_events = []
    now = time.time()

    for event in events:
        # Don't show all day events
        if event.is_allday():
            continue

        now = time.time()
        # If the event already ended
        if event.end_time < now:
            continue

        # Event won't start for more than show_event_before
        if show_event_before > MIN_DELAY and now + SECONDS_PER_MINUTE * show_event_before < event.start_time:
            continue

        # If the event started more than hide_event_after ago
        if hide_event_after > MIN_DELAY and event.start_time + SECONDS_PER_MINUTE * hide_event_after < now:
            continue

        future_events.append(event)

    return future_events

def get_closest(events: List[Event]) -> Optional[Event]:
    closest = None
    for event in events:
        if closest is None or event.start_time < closest.start_time:
            closest = event

    return closest

def from_json(event_json : Dict[str,Any]) -> Event:
    end_time = int(get_unix_time(
        event_json["end"].get("dateTime", event_json["end"].get("date")))
    )
    start_time = event_json["start"].get("dateTime", event_json["start"].get("date"))
    start_time = int(get_unix_time(start_time))

    location = None

    if "location" in event_json:
        location = event_json["location"]
    elif "description" in event_json:
        matches = re.findall(URL_REGEX, event_json["description"])
        location = matches[0][0] if matches else None
    return Event(event_json.get("summary", "(No title)"), start_time, end_time, location)


text:
from pathlib import Path

from typing import List
import datetime
import pickle

from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build, Resource

from event import Event, from_json
from config import CONF_DIR

SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"]
TMP_TOKEN = f"{CONF_DIR}/i3agenda_google_token.pickle"

def connect(credspath: str) -> Resource:
    creds = get_credentials(credspath)

    service = build("calendar", "v3", credentials=creds)
    return service

def get_credentials(credspath):
    creds = None
    # The file token.pickle stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.

    Path(CONF_DIR).mkdir(parents=True, exist_ok=True)
    if Path(TMP_TOKEN).is_file():
        with open(TMP_TOKEN, "rb") as token:
            creds = pickle.load(token)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if not Path(credspath).is_file():
            print(
                """You need to download your credentials json file from the Google API Console and pass its path to this script"""
            )
            exit(1)
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(credspath, SCOPES)
            creds = flow.run_local_server(port=0)
        # Save the credentials for the next run
        with open(TMP_TOKEN, "wb") as token:
            pickle.dump(creds, token)
    return creds

def get_callendar_ids(allowed_calendars_ids: List[str], service: Resource) -> List:
    calendar_ids = []
    while True:
        calendar_list = service.calendarList().list().execute()
        for calendar_list_entry in calendar_list["items"]:
            if (
                not allowed_calendars_ids
                or calendar_list_entry["id"] in allowed_calendars_ids
            ):
                calendar_ids.append(calendar_list_entry["id"])
        page_token = calendar_list.get("nextPageToken")
        if not page_token:
            break
    return calendar_ids

def get_result(service, calendar_id, max_results, time_max_rfc3339=None):
    now = datetime.datetime.utcnow()
    now_rfc3339 = now.isoformat() + "Z"  # 'Z' indicates UTC time
    return (
        service.events()
        .list(
            calendarId=calendar_id,
            timeMin=now_rfc3339,
            timeMax=time_max_rfc3339,
            maxResults=max_results,
            singleEvents=True,
            orderBy="startTime",
        )
        .execute()
    )

def get_today_events(service, calendar_id, max_results):
    now = datetime.datetime.utcnow()
    midnight_rfc3339 = now.replace(hour=23, minute=59, second=59).isoformat() + "Z"
    return get_result(service, calendar_id, max_results, midnight_rfc3339).get(
        "items", []
    )

def get_event_result(service, calendar_id, max_results):
    return get_result(service, calendar_id, max_results).get("items", [])

def get_all_events(calendar_ids, service, max_results, today_only):
    all_events = []

    for calendar_id in calendar_ids:
        if today_only:
            events = get_today_events(service, calendar_id, max_results)
        else:
            events = get_event_result(service, calendar_id, max_results)

        if not events:
            continue

        for event in events:
            all_events.append(from_json(event))

    return all_events

def get_events(
    credentials: str,
    allowed_calendars_ids: List[str],
    max_results: int,
    today_only=False,
) -> List[Event]:
    service = connect(credentials)

    calendar_ids = get_callendar_ids(allowed_calendars_ids, service)

    all_events = get_all_events(calendar_ids, service, max_results, today_only)

    return all_events


text:
#!/usr/bin/env python3

from __future__ import print_function

import subprocess
import config

from typing import List, Optional
import datetime

from event import Event, get_closest, sort_events, get_future_events

from typing import Union
from const import *

DEFAULT_CAL_WEBPAGE = "https://calendar.google.com/calendar/r/day"

def button_action(button_code: str, closest: Event):
    if button_code != "":
        if button_code == LEFT_MOUSE_BUTTON:
            print("Opening calendar page...")
            subprocess.Popen(["xdg-open", DEFAULT_CAL_WEBPAGE])
        elif button_code == RIGHT_MOUSE_BUTTON:
            if closest.location:
                print("Opening location link...")
                subprocess.Popen(["xdg-open", closest.location])

def filter_only_todays_events(events: List[Event]) -> Optional[List[Event]]:
    now = datetime.datetime.now()
    midnight_rfc3339 = now.replace(hour=23, minute=59, second=59)
    return list(
        filter(
            lambda event: datetime.datetime.fromtimestamp(event.start_time)
            < midnight_rfc3339,
            events,
        )
    )

def load_events(args) -> List[Event]:
    from API import get_events
    from cache_utils import load_cache, save_cache

    events : Union[None,list[Event]] = None

    if not args.update:
        events = load_cache(args.cachettl)
        if args.today:
            if events:
                events = filter_only_todays_events(events)

    if events is None or args.update:
        events = get_events(args.credentials, args.ids, args.maxres, args.today)
        save_cache(events)
    return events

def main():
    args = config.parser.parse_args()
    config.CONF_DIR = args.conf

    events = load_events(args)

    events = get_future_events(events, args.hide_event_after, args.show_event_before)

    if args.skip > 0:
        events = sort_events(events)
        events = events[args.skip :]

    closest = get_closest(events)
    if closest is None:
        print(args.no_event_text)
        return

    button_action(config.button, closest)

    print(
        closest.get_string(
            args.limchar,
            args.date_format,
            args.ongoing_time_left,
            args.next_event_time_left,
        )
    )

    if closest.is_urgent():
        # special i3blocks exit code to set the block urgent
        exit(33)

if __name__ == "__main__":
    main()


text:
from config import CONF_DIR
from typing import Optional, List, TextIO

import os.path
import time
import json

from event import Event, EventEncoder
from const import SECONDS_PER_MINUTE

CACHE_PATH = f"{CONF_DIR}/i3agenda_cache.txt"

def load_cache(cachettl: int) -> Optional[List[Event]]:
    if not os.path.exists(CACHE_PATH):
        return None

    if time.time() - os.path.getmtime(CACHE_PATH) > cachettl * SECONDS_PER_MINUTE:
        return None

    try:
        with open(CACHE_PATH, "r") as f:
            events = get_events_from_cache(f)
        return events
    except IOError:
        # Invalid cache
        return None

def save_cache(events: List[Event]):
    with open(CACHE_PATH, "w+") as f:
        f.write(EventEncoder().encode(events))

def get_events_from_cache(f: TextIO):
    events = []
    raw = json.loads(f.read())
    for event in raw:
        try:
            events.append(
                Event(
                    event["summary"],
                    event["start_time"],
                    event["end_time"],
                    event["location"],
                )
            )
        except KeyError:
            # At least one of the events in cache are invalid, must mean that the entire cache is invalid
            return None
    return events


text:
import datetime as dt
import time
from typing_extensions import LiteralString

from const import *

def human_delta(tdelta : dt.timedelta) -> str:
    duration = [ 0 ] * 4  # will hold decomposition of tdelta in d, h, m, s
    fmts = ['{d[0]} day(s)', '{d[1]}h', '{d[2]}m', '{d[3]}s']

    total_seconds = int(tdelta.total_seconds())

    # convert total_seconds in d, h, m ,s
    duration[0], rem = divmod(total_seconds, SECONDS_PER_DAY)
    duration[1], rem = divmod(rem, SECONDS_PER_HOUR)
    duration[2], duration[3] = divmod(rem, SECONDS_PER_MINUTE)

    # Keep only format for non null value
    fmt =  ' '.join([ fmts[i] for i in range(len(duration)) if duration[i] > 0])
    if not fmt:
        return "0m"

    return fmt.format(d = duration)

def make_tz_backward_compatible(full_time : str) -> str:
    # Python introduced the ability to parse ":" in the timezone format (in strptime()) only from version 3.7 and up.
    # We need to remove the : before the timezone to support older versions
    # See https://stackoverflow.com/questions/30999230/how-to-parse-timezone-with-colon for more information
    if full_time[-3] == ":":
        full_time = full_time[:-3] + full_time[-2:]
    return full_time

def get_unix_time(full_time: str) -> float:
    if "T" in full_time:
        event_time_format = "%Y-%m-%dT%H:%M:%S%z"
    else:
        event_time_format = "%Y-%m-%d"

    full_time = make_tz_backward_compatible(full_time)

    return time.mktime(
        dt.datetime.strptime(full_time, event_time_format).astimezone().timetuple()
    )


text:
from typing_extensions import Final

DAYS_PER_WEEK : Final = 7
HOURS_PER_DAY : Final = 24
SECONDS_PER_DAY : Final = 86400
SECONDS_PER_HOUR : Final = 3600
SECONDS_PER_MINUTE : Final = 60

URGENT_DELAY_MN : Final = 5

MIN_CHARS : Final = -1
MIN_DELAY : Final = -1

LEFT_MOUSE_BUTTON : Final = "1"
RIGHT_MOUSE_BUTTON : Final = "3"


text:
import datetime as dt
import os
import time

from freezegun import freeze_time
import pytest

from event import *

os.environ['TZ'] = 'UTC'
time.tzset()

def new_event(start_time: str, end_time: str, summary="summary", location=None):
    start = dt.datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S")
    end = dt.datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S")
    return Event(summary, int(start.timestamp()), int(end.timestamp()), location)

@pytest.mark.parametrize(
    "start,end,expected",
    [
        ("2022-12-14 12:00:07", "2022-12-14 12:05:07", True),
        ("2022-12-13 12:00:07", "2022-12-13 12:05:07", False),  # Before
        ("2022-12-15 12:00:07", "2022-12-15 12:05:07", False),  # After
    ],
)
@freeze_time("2022-12-14 12:03:07")
def test_is_ongoing(start, end, expected):
    assert new_event(start, end).is_ongoing() == expected

@pytest.mark.parametrize(
    "start,end,expected",
    [
        ("2022-12-14 12:00:07", "2022-12-14 12:05:07", True),
        ("2022-12-14 12:00:07", "2022-12-15 12:05:07", True),
        ("2022-12-13 12:00:07", "2022-12-14 12:05:07", False),
        ("2022-12-13 12:00:07", "2022-12-13 12:05:07", False),
    ],
)
@freeze_time("2022-12-14 12:03:07")
def test_is_today(start, end, expected):
    assert new_event(start, end).is_today() == expected

@pytest.mark.parametrize(
    "start,end,expected",
    [
        ("2022-12-15 12:00:07", "2022-12-15 12:05:07", True),
        ("2022-12-15 12:00:07", "2022-12-17 12:05:07", True),
        ("2022-12-14 12:00:07", "2022-12-14 12:05:07", False),
        ("2022-12-14 12:00:07", "2022-12-15 12:05:07", False),
        ("2022-12-13 12:00:07", "2022-12-14 12:05:07", False),
        ("2022-12-13 12:00:07", "2022-12-13 12:05:07", False),
    ],
)
@freeze_time("2022-12-14 12:03:07")
def test_is_tomorrow(start, end, expected):
    assert new_event(start, end).is_tomorrow() == expected

@pytest.mark.parametrize(
    "start,end,expected",
    [
        ("2022-12-14 12:00:07", "2022-12-15 12:05:07", True),
        ("2022-12-14 12:00:07", "2022-12-26 12:05:07", True),
        ("2022-12-20 12:00:07", "2022-12-26 12:05:07", True),
        ("2022-12-22 12:00:07", "2022-12-26 12:05:07", False),
        ("2022-12-14 11:00:07", "2022-12-15 12:05:07", True),
        ("2022-12-13 11:00:07", "2022-12-15 12:05:07", False),
    ],
)
@freeze_time("2022-12-14 12:03:07")
def test_is_this_week(start, end, expected):
    assert new_event(start, end).is_this_week() == expected

@pytest.mark.parametrize(
    "start,end,expected",
    [
        ("2022-12-14 12:09:07", "2022-12-15 12:35:07", True),
        ("2022-12-14 12:05:06", "2022-12-15 12:35:07", False),  # Just before now - 5m
        ("2022-12-14 12:05:08", "2022-12-15 12:35:07", True),  # Just after now - 5m
        ("2022-12-14 12:15:06", "2022-12-15 12:35:07", True),  # Just before now + 5m
        ("2022-12-14 12:15:08", "2022-12-15 12:35:07", False),  # After now + 5m
    ],
)
@freeze_time("2022-12-14 12:10:07")
def test_is_urgent(start, end, expected):
    assert new_event(start, end).is_urgent() == expected

@pytest.mark.parametrize(
    "start,end,expected",
    [
        ("2022-12-14 00:00:00", "2022-12-15 00:00:00", True),
        ("2022-12-14 12:05:06", "2022-12-15 12:35:07", False),
        ("2022-12-14 00:00:00", "2022-12-18 00:00:00", True),
        ("2022-12-14 00:00:00", "2023-12-18 00:00:00", True),
        ("2022-12-14 00:00:00", "2023-12-14 00:00:24", False),
        ("2022-12-14 00:00:00", "2022-12-18 00:00:24", False),
    ],
)
@freeze_time("2022-12-14 12:10:07")
def test_is_allday(start, end, expected):
    assert new_event(start, end).is_allday() == expected

@freeze_time("2022-12-14 12:10:07")
def test_get_future_events_without_after_before():
    events = [
        new_event("2022-12-15 00:00:00", "2022-12-18 00:00:00"),  # all day
        new_event("2022-12-13 00:00:00", "2022-12-13 03:00:00"),  # finished
        new_event("2022-12-14 12:00:00", "2022-12-14 15:00:00"),  # in middle
        new_event("2022-12-14 15:00:00", "2022-12-14 17:00:00"),  # future
    ]
    # It should only keep the two last
    expected = events[-2:]
    assert get_future_events(events, MIN_DELAY, MIN_DELAY) == expected

@freeze_time("2022-12-14 12:10:07")
def test_get_future_events_with_after():
    events = [
        new_event("2022-12-15 00:00:00", "2022-12-18 00:00:00"),  # all day
        new_event("2022-12-13 00:00:00", "2022-12-13 03:00:00"),  # finished
        new_event(
            "2022-12-14 12:00:00", "2022-12-14 15:00:00"
        ),  # in middle more than 5m
        new_event(
            "2022-12-14 12:06:00", "2022-12-14 15:00:00"
        ),  # in middle less than 5m
        new_event("2022-12-14 15:00:00", "2022-12-14 17:00:00"),  # future
    ]
    # It should only keep the two last
    expected = events[-2:]
    assert get_future_events(events, 5, MIN_DELAY) == expected

@freeze_time("2022-12-14 12:10:07")
def test_get_future_events_with_before():
    events = [
        new_event("2022-12-15 00:00:00", "2022-12-18 00:00:00"),  # all day
        new_event("2022-12-13 00:00:00", "2022-12-13 03:00:00"),  # finished
        new_event("2022-12-15 15:00:00", "2022-12-15 17:00:00"),  # distant future
        new_event("2022-12-14 12:15:08", "2022-12-14 17:00:00"),  # future after 5m
        new_event("2022-12-14 12:00:00", "2022-12-14 15:00:00"),  # in middle
        new_event("2022-12-14 12:15:06", "2022-12-14 17:00:00"),  # future before 5m
    ]
    # It should only keep the two  last
    expected = events[-2:]
    assert get_future_events(events, MIN_DELAY, 5) == expected

def test_get_closest():
    events = [
        new_event("2022-12-15 00:00:00", "2022-12-18 00:00:00"),
        new_event("2022-12-13 00:00:00", "2022-12-13 03:00:00"),
        new_event("2022-12-15 15:00:00", "2022-12-15 17:00:00"),
        new_event("2022-12-14 12:15:08", "2022-12-14 17:00:00"),
        new_event("2022-12-14 12:00:00", "2022-12-14 15:00:00"),
        new_event("2022-11-14 12:15:06", "2022-12-14 17:00:00"),  # closest
    ]
    expected = events[-1]
    assert get_closest(events) == expected

@pytest.mark.parametrize(
    "json,expected",
    [
        # Test with a basic input
        (
            {
                "summary": "Test event",
                "start": {"dateTime": "2022-12-05T15:00:00+0000"},
                "end": {"dateTime": "2022-12-05T17:00:00+0000"},
                "location": "Test location",
            },
            Event("Test event", 1670252400, 1670259600, "Test location"),
        ),
        # Test with a different time format
        (
            {
                "summary": "Test event",
                "start": {"date": "2022-12-05"},
                "end": {"date": "2022-12-06"},
                "location": "Test location",
            },
            Event("Test event", 1670198400, 1670284800, "Test location"),
        ),
        # Test with no title
        (
            {
                "start": {"dateTime": "2022-12-05T15:00:00+0100"},
                "end": {"dateTime": "2022-12-05T17:00:00+0100"},
                "location": "Test location",
            },
            Event("(No title)", 1670248800, 1670256000, "Test location"),
        ),
        # Test with a description but no location
        (
            {
                "summary": "Test event",
                "start": {"dateTime": "2022-12-05T15:00:00+0000"},
                "end": {"dateTime": "2022-12-05T17:00:00+0000"},
                "description": "This is a test description with no location.",
            },
            Event("Test event", 1670252400, 1670259600, None),
        ),
        # Test with a location within description
        (
            {
                "summary": "Test event",
                "start": {"dateTime": "2022-12-05T15:00:00+0000"},
                "end": {"dateTime": "2022-12-05T17:00:00+0000"},
                "description": "This is a test description with url : https://truc.com/calendar/ezfzerazdfz.",
            },
            Event(
                "Test event",
                1670252400,
                1670259600,
                "https://truc.com/calendar/ezfzerazdfz.",
            ),
        ),
    ],
)
def test_from_json(json, expected):
    assert from_json(json) == expected

now = dt.datetime.strptime("2022-12-14 12:10:06+0000", "%Y-%m-%d %H:%M:%S%z")

@pytest.mark.parametrize(
    "event_params,otl,netl,expected",
    [
        # Test event that is ongoing as 1 hour left and ongoing_time_left is True
        (
            dict(
                summary="Event 1",
                start_time=now.timestamp(),
                end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(),
                location=None,
            ),
            True,
            False,
            "Event 1 (1h left)",
        ),
        # Test event that is not ongoing and is today and next_event_time_left is True
        (
            dict(
                summary="Event 1",
                start_time=(now + dt.timedelta(minutes=30, seconds=1)).timestamp(),
                end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(),
                location=None,
            ),
            False,
            True,
            "Event 1 in 30m",
        ),
        # Test event that is not ongoing and is today and next_event_time_left is True
        (
            dict(
                summary="Event 1",
                start_time=(now + dt.timedelta(minutes=30, seconds=1)).timestamp(),
                end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(),
                location=None,
            ),
            True,
            False,
            "12:40 Event 1",
        ),
        # Test event that is not ongoing and is today and next_event_time_left is True
        (
            dict(
                summary="Event 1",
                start_time=(now + dt.timedelta(minutes=30, seconds=1)).timestamp(),
                end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(),
                location=None,
            ),
            False,
            False,
            "12:40 Event 1",
        ),
        # Test event that is not ongoing and is tomorrow
        (
            dict(
                summary="Event 1",
                start_time=(now + dt.timedelta(days=1, seconds=1)).timestamp(),
                end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(),
                location=None,
            ),
            False,
            False,
            "Tomorrow at 12:10 Event 1",
        ),
        # Test event that is not ongoing, not tommorrow and this week
        (
            dict(
                summary="Event 1",
                start_time=(now + dt.timedelta(days=2, seconds=1)).timestamp(),
                end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(),
                location=None,
            ),
            False,
            False,
            "Fri at 12:10 Event 1",
        ),
        # Test event that is not ongoing, not tommorrow and after this week
        (
            dict(
                summary="Event 1",
                start_time=(now + dt.timedelta(days=8, seconds=1)).timestamp(),
                end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(),
                location=None,
            ),
            False,
            False,
            "2022-12-22 at 12:10 Event 1",
        ),
    ],
)
@freeze_time(now + dt.timedelta(seconds=1))
def test_get_string(event_params, otl: bool, netl: bool, expected):

    event = Event(**event_params)
    result = event.get_string(
        limit_char=100,
        date_format="%Y-%m-%d",
        ongoing_time_left=otl,
        next_event_time_left=netl,
    )
    assert result == expected

@pytest.mark.parametrize(
    "description, expected", [
        ("A string that has more than twenty chars", "2022-12-22 at 12:10 A string that has mo..."),
        ("اجراجوییِ تازه مغامرة جديدة", "2022-12-22 at 12:10 ...رماغم هزات ِییوجارجا"), # An RTL string
        ("הרפתקה חדשה הרפתקה חדשה", "2022-12-22 at 12:10 ...ח הקתפרה השדח הקתפרה") # An RTL string
    ])
@freeze_time(now + dt.timedelta(seconds=1))
def test_get_string_description(description, expected):
    event = Event(
                summary=description,
                start_time=int((now + dt.timedelta(days=8, seconds=1)).timestamp()),
                end_time=int((now + dt.timedelta(hours=1, seconds=1)).timestamp()),
                location=None,
                )
    result = event.get_string(
        limit_char=20,
        date_format="%Y-%m-%d",
        ongoing_time_left=False,
        next_event_time_left=False,
    )
    assert result == expected


text:
import datetime as dt

import pytest

from typing import Dict

from helpers import *

@pytest.mark.parametrize("test_input,expected",
[
    ({"minutes":0}, "0m"),
    ({"minutes":1}, "1m"),
    ({"hours":1}, "1h"),
    ({"days":1}, "1 day(s)"),
    ({"days":1, "hours":1, "minutes":1}, "1 day(s) 1h 1m"),
    ({"hours":23, "minutes":59}, "23h 59m"),
]
)
def test_human_delta(test_input:Dict[str,int],expected:str):
    assert human_delta(dt.timedelta(**test_input)) == expected


text:
require 'faraday'

connection = Faraday.new('https://example.com', :ssl => {
  # Peer verification is true by default
  :verify => true,
  # Rarely needed. If you need to be explicit, set:
  :verify_mode => OpenSSL::SSL::VERIFY_PEER,

  # Override SSL_CERT_FILE: the path to your CA bundle
  :ca_file => '/path/to/ca_cert.pem',
  # Override SSL_CERT_DIR: the directory with individual cert files
  :ca_path => '/path/to/certs/',

  # Optional. Store extra certificates that you will trust.
  :cert_store => OpenSSL::X509::Store.new,

  # Max length of cert chain to be verified
  :verify_depth => 3,

  # Rarely needed. Set client certificate and private key.
  :client_cert => OpenSSL::X509::Certificate.new,
  :client_key => OpenSSL::PKey::RSA.new
}) do
  conn.request :url_encoded
  conn.response :raise_error
  conn.response :logger
  conn.adapter :net_http
end

connection.get('/hello')

repo_name: mislav/ssl-tools

repo_language: Ruby

file_name: show-cert

mime_type: text/x-shellscript


text:
#!/bin/sh
set -e
while [ -n "$1" ]; do
  if ! [ -f "$1" ]; then
    echo "invalid .cer file: \`$1'" >&2
    exit 1
  fi
  openssl x509 -inform der -in "$1" -outform pem
  shift 1
done


text:
# Usage: ruby doctor.rb [HOST=status.github.com[:PORT=443]]
require 'rbconfig'
require 'net/https'

if ARGV[0] =~ /^[^-]/
  host, port = ARGV[0].split(':', 2)
else
  host = 'status.github.com'
end
port ||= 443

ruby = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'])
ruby_version = RUBY_VERSION
if patch = RbConfig::CONFIG['PATCHLEVEL']
  ruby_version += "-p#{patch}"
end
puts "%s (%s)" % [ruby, ruby_version]

openssl_dir = OpenSSL::X509::DEFAULT_CERT_AREA
mac_openssl = '/System/Library/OpenSSL' == openssl_dir
puts "%s: %s" % [OpenSSL::OPENSSL_VERSION, openssl_dir]
[OpenSSL::X509::DEFAULT_CERT_DIR_ENV, OpenSSL::X509::DEFAULT_CERT_FILE_ENV].each do |key|
  puts "%s=%s" % [key, ENV[key].to_s.inspect]
end

ca_file = ENV[OpenSSL::X509::DEFAULT_CERT_FILE_ENV] || OpenSSL::X509::DEFAULT_CERT_FILE
ca_path = (ENV[OpenSSL::X509::DEFAULT_CERT_DIR_ENV] || OpenSSL::X509::DEFAULT_CERT_DIR).chomp('/')

puts "\nHEAD https://#{host}:#{port}"
http = Net::HTTP.new(host, port)
http.use_ssl = true

# Explicitly setting cert_store like this is not needed in most cases but it
# seems necessary in edge cases such as when using `verify_callback` in some
# combination of Ruby + OpenSSL versions.
http.cert_store = OpenSSL::X509::Store.new
http.cert_store.set_default_paths

http.verify_mode = OpenSSL::SSL::VERIFY_PEER
failed_cert = failed_cert_reason = nil

if mac_openssl
  warn "warning: will not be able show failed certificate info on OS X's OpenSSL"
  # This drives me absolutely nuts. It seems that on Rubies compiled against OS X's
  # system OpenSSL, the mere fact of defining a `verify_callback` makes the
  # cert verification fail for requests that would otherwise be successful.
else
  http.verify_callback = lambda { |verify_ok, store_context|
    if !verify_ok
      failed_cert = store_context.current_cert
      failed_cert_reason = "%d: %s" % [ store_context.error, store_context.error_string ]
    end
    verify_ok
  }
end

user_agent = "net/http #{ruby_version}"
req = Net::HTTP::Head.new('/', 'user-agent' => user_agent)

begin
  res = http.start { http.request(req) }
  abort res.inspect if res.code.to_i >= 500
  puts "OK"
rescue Errno::ECONNREFUSED
  puts "Error: connection refused"
  exit 1
rescue OpenSSL::SSL::SSLError => e
  puts "#{e.class}: #{e.message}"

  if failed_cert
    puts "\nThe server presented a certificate that could not be verified:"
    puts "  subject: #{failed_cert.subject}"
    puts "  issuer: #{failed_cert.issuer}"
    puts "  error code %s" % failed_cert_reason
  end

  ca_file_missing = !File.exist?(ca_file) && !mac_openssl
  ca_path_empty = Dir["#{ca_path}/*"].empty?

  if ca_file_missing || ca_path_empty
    puts "\nPossible causes:"
    puts "  `%s' does not exist" % ca_file if ca_file_missing
    puts "  `%s/' is empty" % ca_path if ca_path_empty
  end

  exit 1
end


text:
# SSL tools

    der2pem FILES...

    remote-cert status.github.com | show-cert

    create-cert example.com "My Organization" > cert_and_key.pem

    ruby doctor.rb example.com:443

Sample output of `doctor.rb`:

```
/Users/mislav/.rbenv/versions/2.0.0-p247/bin/ruby (2.0.0-p247)
OpenSSL 1.0.1e 11 Feb 2013: /usr/local/etc/openssl
SSL_CERT_DIR=""
SSL_CERT_FILE=""

HEAD https://status.github.com:443
OpenSSL::SSL::SSLError: SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed

The server presented a certificate that could not be verified:
subject: /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert High Assurance CA-3
issuer: /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert High Assurance EV Root CA
error code 20: unable to get local issuer certificate

Possible causes:
- `/usr/local/etc/openssl/cert.pem' does not exist
- `/usr/local/etc/openssl/certs/' is empty
```


text:
#!/bin/sh
# Usage: remote-cert HOST[:PORT] [HOST[:PORT] ...]
set -e

while [ -n "$1" ]; do
  host="${1%:*}"
  port="${1#*:}"
  [ "$host" = "$port" ] && port=443

  echo | openssl s_client -connect ${host}:${port} 2>&1 |\
    sed -nEe '/^-+BEGIN/,/^-+END/p'

  shift 1
done