├── README.md ├── all_for_testing.d ├── apng.d ├── archive.d ├── argon2.d ├── arsd.ddoc ├── audio.d ├── blendish.d ├── bmp.d ├── calendar.d ├── cgi.d ├── characterencodings.d ├── cidr.d ├── cli.d ├── color.d ├── com.d ├── comhelpers.d ├── conv.d ├── core.d ├── csv.d ├── curl.d ├── database.d ├── database_generation.d ├── dbus.d ├── dds.d ├── declarativeloader.d ├── discord.d ├── docs ├── dev_philosophy.d ├── general_concepts.d └── package.d ├── docx.d ├── dom.d ├── dub.json ├── email.d ├── engine.d ├── english.d ├── eventloop.d ├── exception.d ├── fibersocket.d ├── file.d ├── game.d ├── gamehelpers.d ├── generatedomcases.d ├── gpio.d ├── hmac.d ├── html.d ├── htmltotext.d ├── htmlwidget.d ├── http.d ├── http2.d ├── ico.d ├── image.d ├── imageresize.d ├── ini.d ├── jni.d ├── joystick.d ├── jpeg.d ├── jpg.d ├── jsvar.d ├── libeay32.dll ├── libssh2.d ├── libssh2.dll ├── libssh2.lib ├── mailserver.d ├── mangle.d ├── markdown.d ├── midi.d ├── midiplayer.d ├── minigui.d ├── minigui_addons ├── color_dialog.d ├── datetime_picker.d ├── keyboard_palette_widget.d ├── nanovega.d ├── package.d ├── terminal_emulator_widget.d └── webview.d ├── minigui_xml.d ├── mp3.d ├── mssql.d ├── mvd.d ├── mysql.d ├── nanovega.d ├── nukedopl3.d ├── oauth.d ├── package.d ├── pcx.d ├── pixmappaint.d ├── pixmappresenter.d ├── pixmaprecorder.d ├── png.d ├── postgres.d ├── pptx.d ├── qrcode.d ├── querygenerator.d ├── random.d ├── reggaefile.d ├── rpc.d ├── rss.d ├── rtf.d ├── rtud.d ├── screen.d ├── script.d ├── sha.d ├── shell.d ├── simpleaudio.d ├── simpledisplay.d ├── sqlite.d ├── sslsocket.d ├── stb_truetype.d ├── string.d ├── svg.d ├── targa.d ├── terminal.d ├── terminalemulator.d ├── textlayouter.d ├── ttf.d ├── uda.d ├── uri.d ├── vorbis.d ├── wav.d ├── web.d ├── web.d.php ├── webtemplate.d ├── webview.d ├── wmutil.d ├── xlsx.d ├── xwindows.d └── zip.d /all_for_testing.d: -------------------------------------------------------------------------------- 1 | /++ 2 | This is a dummy module just used to test every active file in the collection. 3 | 4 | Do not use it for anything else. 5 | +/ 6 | module arsd.all_for_testing; 7 | 8 | import arsd.apng; 9 | import arsd.archive; 10 | version(linux) import arsd.argon2; // not implemented on other OS 11 | // import arsd.audio; // D1 or 2.098 12 | import arsd.blendish; 13 | import arsd.bmp; 14 | import arsd.calendar; 15 | import arsd.cgi; 16 | import arsd.characterencodings; 17 | import arsd.cidr; 18 | import arsd.cli; 19 | import arsd.color; 20 | import arsd.com; 21 | import arsd.comhelpers; 22 | import arsd.conv; 23 | import arsd.core; 24 | import arsd.csv; 25 | import arsd.curl; 26 | import arsd.database; 27 | import arsd.database_generation; 28 | import arsd.dbus; 29 | import arsd.dds; 30 | import arsd.declarativeloader; 31 | import arsd.discord; 32 | import arsd.docs.dev_philosophy; 33 | import arsd.docs.general_concepts; 34 | import arsd.docs; 35 | import arsd.docx; 36 | import arsd.dom; 37 | import arsd.email; 38 | // import arsd.engine; // D1 or 2.098 39 | import arsd.english; 40 | import arsd.eventloop; 41 | // import arsd.exception; // deprecated 42 | import arsd.fibersocket; 43 | import arsd.file; 44 | import arsd.game; 45 | import arsd.gamehelpers; 46 | import arsd.gpio; 47 | import arsd.hmac; 48 | import arsd.html; 49 | import arsd.htmltotext; 50 | // import arsd.htmlwidget; // requires special version of dom.d and obsolete anyway, use my other browser stuff instead like jambrowser 51 | import arsd.http; 52 | import arsd.http2; 53 | import arsd.ico; 54 | import arsd.image; 55 | import arsd.imageresize; 56 | import arsd.ini; 57 | import arsd.jni; 58 | import arsd.joystick; 59 | import arsd.jpeg; 60 | import arsd.jpg; 61 | import arsd.jsvar; 62 | import arsd.mailserver; 63 | import arsd.mangle; 64 | import arsd.markdown; 65 | import arsd.midi; 66 | import arsd.midiplayer; 67 | import arsd.minigui; 68 | import arsd.minigui_addons.color_dialog; 69 | import arsd.minigui_addons.datetime_picker; 70 | import arsd.minigui_addons.keyboard_palette_widget; 71 | import arsd.minigui_addons.nanovega; 72 | import arsd.minigui_addons; 73 | import arsd.minigui_addons.terminal_emulator_widget; 74 | import arsd.minigui_addons.webview; 75 | import arsd.minigui_xml; 76 | import arsd.mp3; 77 | import arsd.mssql; 78 | import arsd.mvd; 79 | import arsd.mysql; 80 | import arsd.nanovega; 81 | import arsd.nukedopl3; 82 | import arsd.oauth; 83 | import arsd; 84 | import arsd.pcx; 85 | import arsd.pixmappaint; 86 | import arsd.pixmappresenter; 87 | import arsd.pixmaprecorder; 88 | import arsd.png; 89 | import arsd.postgres; 90 | import arsd.pptx; 91 | import arsd.qrcode; 92 | import arsd.querygenerator; 93 | import arsd.random; 94 | import arsd.rpc; 95 | import arsd.rss; 96 | import arsd.rtf; 97 | // import arsd.rtud; // totally obsolete 98 | // import arsd.screen; // D1 or 2.098 99 | import arsd.script; 100 | import arsd.sha; 101 | import arsd.simpleaudio; 102 | import arsd.simpledisplay; 103 | import arsd.sqlite; 104 | // import arsd.sslsocket; // obsolete 105 | // import arsd.stb_truetype; // obsolete 106 | import arsd.string; 107 | import arsd.svg; 108 | import arsd.targa; 109 | import arsd.terminal; 110 | import arsd.terminalemulator; 111 | import arsd.textlayouter; 112 | import arsd.ttf; 113 | import arsd.uda; 114 | import arsd.uri; 115 | import arsd.vorbis; 116 | import arsd.wav; 117 | import arsd.web; 118 | import arsd.webtemplate; 119 | import arsd.webview; 120 | import arsd.wmutil; 121 | import arsd.xlsx; 122 | import arsd.xwindows; 123 | import arsd.zip; 124 | -------------------------------------------------------------------------------- /argon2.d: -------------------------------------------------------------------------------- 1 | /++ 2 | My minimal interface to https://github.com/p-h-c/phc-winner-argon2 3 | 4 | You must compile and install the C library separately. 5 | +/ 6 | module arsd.argon2; 7 | 8 | // a password length limitation might legit make sense here cuz of the hashing function can get slow 9 | 10 | // it is conceivably useful to hash the password with a secret key before passing to this function, 11 | // but I'm not going to do that automatically here just to keep this thin and simple. 12 | 13 | import core.stdc.stdint; 14 | 15 | pragma(lib, "argon2"); 16 | 17 | extern(C) 18 | int argon2id_hash_encoded( 19 | const uint32_t t_cost, 20 | const uint32_t m_cost, 21 | const uint32_t parallelism, 22 | const void *pwd, const size_t pwdlen, 23 | const void *salt, const size_t saltlen, 24 | const size_t hashlen, char *encoded, 25 | const size_t encodedlen); 26 | 27 | extern(C) 28 | int argon2id_verify(const char *encoded, const void *pwd, 29 | const size_t pwdlen); 30 | 31 | enum ARGON2_OK = 0; 32 | 33 | /// Parameters to the argon2 function. Bigger numbers make it harder to 34 | /// crack, but also take more resources for legitimate users too 35 | /// (e.g. making logins and signups slower and more memory-intensive). Some 36 | /// examples are provided. HighSecurity is about 3/4 second on my computer, 37 | /// MediumSecurity about 1/3 second, LowSecurity about 1/10 second. 38 | struct SecurityParameters { 39 | uint cpuCost; 40 | uint memoryCost; /// in KiB fyi 41 | uint parallelism; 42 | } 43 | 44 | /// ditto 45 | enum HighSecurity = SecurityParameters(8, 512_000, 8); 46 | /// ditto 47 | enum MediumSecurity = SecurityParameters(4, 256_000, 4); 48 | /// ditto 49 | enum LowSecurity = SecurityParameters(2, 128_000, 4); 50 | 51 | /// Check's a user's provided password against the saved password, and returns true if they matched. Neither string can be empty. 52 | bool verify(string savedPassword, string providedPassword) { 53 | return argon2id_verify((savedPassword[$-1] == 0 ? savedPassword : (savedPassword ~ '\0')).ptr, providedPassword.ptr, providedPassword.length) == ARGON2_OK; 54 | } 55 | 56 | /// encode a password for secure storage. verify later with [verify] 57 | string encode(string password, SecurityParameters params = MediumSecurity) { 58 | char[256] buffer; 59 | enum HASHLEN = 80; 60 | 61 | import core.stdc.string; 62 | 63 | ubyte[32] salt = void; 64 | 65 | version(linux) {{ 66 | import core.sys.posix.unistd; 67 | import core.sys.posix.fcntl; 68 | int fd = open("/dev/urandom", O_RDONLY); 69 | auto ret = read(fd, salt.ptr, salt.length); 70 | assert(ret == salt.length); 71 | close(fd); 72 | }} else version(Windows) {{ 73 | // https://docs.microsoft.com/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptgenrandom 74 | static assert(0); 75 | }} else { 76 | import std.random; 77 | foreach(ref s; salt) 78 | s = cast(ubyte) uniform(0, 256); 79 | 80 | static assert(0, "csrng not implemented"); 81 | } 82 | 83 | auto ret = argon2id_hash_encoded( 84 | params.cpuCost, 85 | params.memoryCost, 86 | params.parallelism, 87 | password.ptr, password.length, 88 | salt.ptr, salt.length, 89 | HASHLEN, // desired size of hash. I think this is fine being arbitrary 90 | buffer.ptr, 91 | buffer.length 92 | ); 93 | 94 | if(ret != ARGON2_OK) 95 | throw new Exception("wtf"); 96 | 97 | return buffer[0 .. strlen(buffer.ptr) + 1].idup; 98 | } 99 | -------------------------------------------------------------------------------- /arsd.ddoc: -------------------------------------------------------------------------------- 1 | DDOC_SECTION=
$0
2 | DDOC_SECTION_H=$0 3 | DDOC_BLANKLINE=

4 | DDOC_ANCHOR=  5 | D_INLINECODE=$0 6 | DDOC_DECL= $(DT $0) 7 | DDOC_DECL_DD= $(DD $0) 8 | DDOC_PARAMS=$0
Parameters
9 | DDOC_PARAM_ROW=$0 10 | DDOC_PARAM_ID=$0 11 | DDOC_PARAM=$0 12 | DDOC_PSYMBOL=$0 13 | DDOC= 14 | 15 | 82 | $(TITLE) 83 | 84 | 85 |

86 |

$(TITLE)

87 | $(BODY) 88 |
89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /audio.d: -------------------------------------------------------------------------------- 1 | /// Part of my old D1 game library along with [arsd.screen] and [arsd.engine]. Do not use in new projects. 2 | module arsd.audio; 3 | 4 | import sdl.SDL; 5 | import sdl.SDL_mixer; 6 | 7 | import arsd.engine; 8 | 9 | bool audioIsLoaded; // potential hack material 10 | 11 | class Sound { 12 | public: 13 | this(char[] filename){ 14 | if(!audioIsLoaded) 15 | return; 16 | sfx = Mix_LoadWAV((filename ~ "\0").ptr); 17 | if(sfx is null) 18 | throw new Exception(immutableString("Sound load " ~ filename)); 19 | } 20 | 21 | /* 22 | this(Wav wav){ 23 | auto w = wav.toMemory; 24 | SDL_RWops* a = SDL_RWFromMem(w, w.length) 25 | if(a is null) throw new Exception("sdl rw ops"); 26 | scope(exit) SDL_FreeRW(a); 27 | sfx = Mix_LoadWAV_RW(a, 0); 28 | if(sfx is null) throw new Exception("loadwav rw"); 29 | } 30 | */ 31 | 32 | ~this(){ 33 | if(sfx !is null) 34 | Mix_FreeChunk(sfx); 35 | } 36 | 37 | private: 38 | Mix_Chunk* sfx; 39 | } 40 | 41 | class Music { 42 | public: 43 | this(char[] filename){ 44 | if(!audioIsLoaded) 45 | return; 46 | mus = Mix_LoadMUS((filename~"\0").ptr); 47 | if(mus is null) 48 | throw new Exception(immutableString("Music load " ~ filename)); 49 | } 50 | 51 | ~this(){ 52 | if(mus !is null) 53 | Mix_FreeMusic(mus); 54 | } 55 | private: 56 | Mix_Music* mus; 57 | } 58 | 59 | class Audio{ 60 | public: 61 | this(bool act = true){ 62 | if(audioIsLoaded) 63 | throw new Exception("Cannot load audio twice"); 64 | 65 | if(!act){ 66 | audioIsLoaded = false; 67 | active = false; 68 | return; 69 | } 70 | if(1) { // if(Mix_OpenAudio(22050, AUDIO_S16SYS, 2, 4096/2 /* the /2 is new */) != 0){ 71 | active = false; //throw new Error; 72 | error = true; 73 | audioIsLoaded = false; 74 | } else { 75 | active = true; 76 | error = false; 77 | audioIsLoaded = true; 78 | } 79 | 80 | sfxChannel = 1; 81 | 82 | careAboutErrors = false; 83 | } 84 | 85 | void activate(){ 86 | if(!audioIsLoaded) return; 87 | if(!error) 88 | active = true; 89 | } 90 | 91 | void deactivate(){ 92 | if(!audioIsLoaded) return; 93 | active = false; 94 | } 95 | 96 | void toggleActivation(){ 97 | if(!audioIsLoaded) return; 98 | if(error) 99 | return; 100 | active = !active; 101 | } 102 | 103 | ~this(){ 104 | if(audioIsLoaded){ 105 | Mix_HaltMusic(); 106 | Mix_HaltChannel(-1); 107 | Mix_CloseAudio(); 108 | } 109 | } 110 | 111 | void playEffect(Sound snd, bool loop = false){ 112 | if(!active || snd is null) 113 | return; 114 | 115 | //if(Mix_Playing(sfxChannel)) 116 | // return; 117 | 118 | sfxChannel = Mix_PlayChannel(-1, snd.sfx, loop == true ? -1 : 0); 119 | 120 | } 121 | void stopEffect(){ 122 | if(!active) 123 | return; 124 | 125 | Mix_HaltChannel(sfxChannel); 126 | } 127 | 128 | void playMusic(Music mus, bool loop = true){ 129 | if(!active || mus is null) 130 | return; 131 | 132 | if(Mix_PlayMusic(mus.mus, loop == true ? -1 : 0) == -1) 133 | throw new Exception("play music"); 134 | // musicIsPlaying = false; 135 | else 136 | musicIsPlaying = true; 137 | } 138 | 139 | void pauseMusic(){ 140 | if(!active) 141 | return; 142 | 143 | if(musicIsPlaying){ 144 | Mix_PauseMusic(); 145 | musicIsPaused = true; 146 | } 147 | } 148 | 149 | void unpauseMusic(){ 150 | if(!active) 151 | return; 152 | 153 | if(musicIsPaused){ 154 | Mix_ResumeMusic(); 155 | musicIsPaused = false; 156 | } 157 | } 158 | 159 | void stopMusic(){ 160 | if(!active) 161 | return; 162 | 163 | Mix_HaltMusic(); 164 | } 165 | 166 | 167 | void stopAll(){ 168 | if(!active) 169 | return; 170 | 171 | Mix_HaltMusic(); 172 | Mix_HaltChannel(-1); 173 | } 174 | 175 | private: 176 | int sfxChannel; 177 | bool active; 178 | bool error; 179 | 180 | bool musicIsPaused; 181 | bool musicIsPlaying; 182 | 183 | bool careAboutErrors; 184 | } 185 | 186 | int Mix_PlayChannel(int channel, Mix_Chunk* chunk, int loops) { 187 | return Mix_PlayChannelTimed(channel,chunk,loops,-1); 188 | } 189 | Mix_Chunk * Mix_LoadWAV(in char *file) { 190 | return Mix_LoadWAV_RW(SDL_RWFromFile(file, "rb"), 1); 191 | } 192 | -------------------------------------------------------------------------------- /calendar.d: -------------------------------------------------------------------------------- 1 | /++ 2 | 3 | OpenD could use automatic mixin to child class... 4 | 5 | Extensions: color. exrule? trash day - if holiday occurred that week, move it forward a day 6 | 7 | Standards: categories 8 | 9 | UI idea for rrule: show a mini two year block with the day highlighted 10 | -> also just let user click on a bunch of days so they can make a list 11 | 12 | Want ability to add special info to a single item of a recurring event 13 | 14 | Can use inotify to reload ui when sqlite db changes (or a trigger on postgres?) 15 | 16 | https://datatracker.ietf.org/doc/html/rfc5545 17 | https://icalendar.org/ 18 | +/ 19 | module arsd.calendar; 20 | 21 | import arsd.core; 22 | 23 | import std.datetime; 24 | 25 | /++ 26 | History: 27 | Added July 3, 2024 28 | +/ 29 | SimplifiedUtcTimestamp parseTimestampString(string when, SysTime relativeTo) /*pure*/ { 30 | import std.string; 31 | 32 | int parsingWhat; 33 | int bufferedNumber = int.max; 34 | 35 | int secondsCount; 36 | 37 | void addSeconds(string word, int bufferedNumber, int multiplier) { 38 | if(parsingWhat == 0) 39 | parsingWhat = 1; 40 | if(parsingWhat != 1) 41 | throw ArsdException!"unusable timestamp string"("you said 'at' but gave a relative time", when); 42 | if(bufferedNumber == int.max) 43 | throw ArsdException!"unusable timestamp string"("no number before unit", when, word); 44 | secondsCount += bufferedNumber * multiplier; 45 | bufferedNumber = int.max; 46 | } 47 | 48 | foreach(word; when.split(" ")) { 49 | word = strip(word).toLower().replace(",", ""); 50 | if(word == "in") 51 | parsingWhat = 1; 52 | else if(word == "at") 53 | parsingWhat = 2; 54 | else if(word == "and") { 55 | // intentionally blank 56 | } else if(word.indexOf(":") != -1) { 57 | if(secondsCount != 0) 58 | throw ArsdException!"unusable timestamp string"("cannot mix time styles", when, word); 59 | 60 | if(parsingWhat == 0) 61 | parsingWhat = 2; // assume absolute time when this comes in 62 | 63 | bool wasPm; 64 | 65 | if(word.length > 2 && word[$-2 .. $] == "pm") { 66 | word = word[0 .. $-2]; 67 | wasPm = true; 68 | } else if(word.length > 2 && word[$-2 .. $] == "am") { 69 | word = word[0 .. $-2]; 70 | } 71 | 72 | // FIXME: what about midnight? 73 | int multiplier = 3600; 74 | foreach(part; word.split(":")) { 75 | import std.conv; 76 | secondsCount += multiplier * to!int(part); 77 | multiplier /= 60; 78 | } 79 | 80 | if(wasPm) 81 | secondsCount += 12 * 3600; 82 | } else if(word.isNumeric()) { 83 | import std.conv; 84 | bufferedNumber = to!int(word); 85 | } else if(word == "seconds" || word == "second") { 86 | addSeconds(word, bufferedNumber, 1); 87 | } else if(word == "minutes" || word == "minute") { 88 | addSeconds(word, bufferedNumber, 60); 89 | } else if(word == "hours" || word == "hour") { 90 | addSeconds(word, bufferedNumber, 60 * 60); 91 | } else 92 | throw ArsdException!"unusable timestamp string"("i dont know what this word means", when, word); 93 | } 94 | 95 | if(parsingWhat == 0) 96 | throw ArsdException!"unusable timestamp string"("couldn't figure out what to do with this input", when); 97 | 98 | else if(parsingWhat == 1) // relative time 99 | return SimplifiedUtcTimestamp((relativeTo + seconds(secondsCount)).stdTime); 100 | else if(parsingWhat == 2) { // absolute time (assuming it is today in our time zone) 101 | auto today = relativeTo; 102 | today.hour = 0; 103 | today.minute = 0; 104 | today.second = 0; 105 | return SimplifiedUtcTimestamp((today + seconds(secondsCount)).stdTime); 106 | } else 107 | assert(0); 108 | } 109 | 110 | unittest { 111 | auto testTime = SysTime(DateTime(Date(2024, 07, 03), TimeOfDay(10, 0, 0)), UTC()); 112 | void test(string what, string expected) { 113 | auto result = parseTimestampString(what, testTime).toString; 114 | assert(result == expected, result); 115 | } 116 | 117 | test("in 5 minutes", "2024-07-03T10:05:00Z"); 118 | test("in 5 minutes and 5 seconds", "2024-07-03T10:05:05Z"); 119 | test("in 5 minutes, 45 seconds", "2024-07-03T10:05:45Z"); 120 | test("at 5:44", "2024-07-03T05:44:00Z"); 121 | test("at 5:44pm", "2024-07-03T17:44:00Z"); 122 | } 123 | 124 | version(none) 125 | void main() { 126 | auto e = new CalendarEvent( 127 | start: DateTime(2024, 4, 22), 128 | end: Date(2024, 04, 22), 129 | ); 130 | } 131 | 132 | class Calendar { 133 | CalendarEvent[] events; 134 | } 135 | 136 | /++ 137 | 138 | +/ 139 | class CalendarEvent { 140 | DateWithOptionalTime start; 141 | DateWithOptionalTime end; 142 | 143 | Recurrence recurrence; 144 | 145 | int color; 146 | string title; // summary 147 | string details; 148 | 149 | string uid; 150 | 151 | this(DateWithOptionalTime start, DateWithOptionalTime end, Recurrence recurrence = Recurrence.none) { 152 | this.start = start; 153 | this.end = end; 154 | this.recurrence = recurrence; 155 | } 156 | } 157 | 158 | /++ 159 | 160 | +/ 161 | struct DateWithOptionalTime { 162 | string tzlocation; 163 | DateTime dt; 164 | bool hadTime; 165 | 166 | @implicit 167 | this(DateTime dt) { 168 | this.dt = dt; 169 | this.hadTime = true; 170 | } 171 | 172 | @implicit 173 | this(Date d) { 174 | this.dt = DateTime(d, TimeOfDay.init); 175 | this.hadTime = false; 176 | } 177 | 178 | this(in char[] s) { 179 | // FIXME 180 | } 181 | } 182 | 183 | /++ 184 | 185 | +/ 186 | struct Recurrence { 187 | static Recurrence none() { 188 | return Recurrence.init; 189 | } 190 | } 191 | 192 | /+ 193 | 194 | enum FREQ { 195 | 196 | } 197 | 198 | struct RRULE { 199 | FREQ freq; 200 | int interval; 201 | int count; 202 | DAY wkst; 203 | 204 | // these can be negative too indicating the xth from the last... 205 | DAYSET byday; // ubyte bitmask... except it can also have numbers atached wtf 206 | 207 | // so like `BYDAY=-2MO` means second-to-last monday 208 | 209 | MONTHDAYSET byMonthDay; // uint bitmask 210 | HOURSET byHour; // uint bitmask 211 | MONTHDSET byMonth; // ushort bitmask 212 | 213 | WEEKSET byWeekNo; // ulong bitmask 214 | 215 | int BYSETPOS; 216 | } 217 | 218 | +/ 219 | 220 | struct ICalParser { 221 | // if the following line starts with whitespace, remove the cr/lf/ and that ONE ws char, then add to the previous line 222 | // it is supposed to support this even if it is in the middle of a utf-8 sequence 223 | // contentline = name *(";" param ) ":" value CRLF 224 | // you're supposed to split lines longer than 75 octets when generating. 225 | 226 | void feedEntireFile(in ubyte[] data) { 227 | feed(data); 228 | feed(null); 229 | } 230 | void feedEntireFile(in char[] data) { 231 | feed(data); 232 | feed(null); 233 | } 234 | 235 | /++ 236 | Feed it some data you have ready. 237 | 238 | Feed it an empty array or `null` to indicate end of input. 239 | +/ 240 | void feed(in char[] data) { 241 | feed(cast(const(ubyte)[]) data); 242 | } 243 | 244 | /// ditto 245 | void feed(in ubyte[] data) { 246 | const(ubyte)[] toProcess; 247 | if(unprocessedData.length) { 248 | unprocessedData ~= data; 249 | toProcess = unprocessedData; 250 | } else { 251 | toProcess = data; 252 | } 253 | 254 | auto eol = toProcess.indexOf("\n"); 255 | if(eol == -1) { 256 | unprocessedData = cast(ubyte[]) toProcess; 257 | } else { 258 | // if it is \r\n, remove the \r FIXME 259 | // if it is \r\n, need to concat 260 | // if it is \r\n\t, also need to concat 261 | processLine(toProcess[0 .. eol]); 262 | } 263 | } 264 | 265 | /// ditto 266 | void feed(typeof(null)) { 267 | feed(cast(const(ubyte)[]) null); 268 | } 269 | 270 | private ubyte[] unprocessedData; 271 | 272 | private void processLine(in ubyte[] line) { 273 | 274 | } 275 | } 276 | 277 | immutable monthNames = [ 278 | "", 279 | "January", 280 | "February", 281 | "March", 282 | "April", 283 | "May", 284 | "June", 285 | "July", 286 | "August", 287 | "September", 288 | "October", 289 | "November", 290 | "December" 291 | ]; 292 | 293 | immutable daysOfWeekNames = [ 294 | "Sunday", 295 | "Monday", 296 | "Tuesday", 297 | "Wednesday", 298 | "Thursday", 299 | "Friday", 300 | "Saturday", 301 | ]; 302 | -------------------------------------------------------------------------------- /cidr.d: -------------------------------------------------------------------------------- 1 | /// Some helper functions for using CIDR format network ranges. 2 | module arsd.cidr; 3 | 4 | /// 5 | uint addressToUint(string address) { 6 | import std.algorithm.iteration, std.conv; 7 | 8 | uint result; 9 | int place = 3; 10 | foreach(part; splitter(address, ".")) { 11 | assert(place >= 0); 12 | result |= to!int(part) << (place * 8); 13 | place--; 14 | } 15 | 16 | return result; 17 | } 18 | 19 | /// 20 | string uintToAddress(uint addr) { 21 | import std.conv; 22 | string res; 23 | res ~= to!string(addr >> 24); 24 | res ~= "."; 25 | res ~= to!string((addr >> 16) & 0xff); 26 | res ~= "."; 27 | res ~= to!string((addr >> 8) & 0xff); 28 | res ~= "."; 29 | res ~= to!string((addr >> 0) & 0xff); 30 | 31 | return res; 32 | } 33 | 34 | /// 35 | struct IPv4Block { 36 | this(string cidr) { 37 | import std.algorithm.searching, std.conv; 38 | auto parts = findSplit(cidr, "/"); 39 | this.currentAddress = addressToUint(parts[0]); 40 | auto count = to!int(parts[2]); 41 | 42 | if(count != 0) { 43 | this.netmask = ((1L << count) - 1) & 0xffffffff; 44 | this.netmask <<= 32-count; 45 | } 46 | 47 | this.startingAddress = this.currentAddress & this.netmask; 48 | 49 | validate(); 50 | 51 | restart(); 52 | } 53 | 54 | this(string address, string netmask) { 55 | this.currentAddress = addressToUint(address); 56 | this.netmask = addressToUint(netmask); 57 | this.startingAddress = this.currentAddress & this.netmask; 58 | 59 | validate(); 60 | 61 | restart(); 62 | } 63 | 64 | void validate() { 65 | if(!isValid()) 66 | throw new Exception("invalid"); 67 | } 68 | 69 | bool isValid() { 70 | return (startingAddress & netmask) == (currentAddress & netmask); 71 | } 72 | 73 | void restart() { 74 | remaining = ~this.netmask - (currentAddress - startingAddress); 75 | } 76 | 77 | @property string front() { 78 | return uintToAddress(currentAddress); 79 | } 80 | 81 | @property bool empty() { 82 | return remaining < 0; 83 | } 84 | 85 | void popFront() { 86 | currentAddress++; 87 | remaining--; 88 | } 89 | 90 | string toString() { 91 | import std.conv; 92 | return uintToAddress(startingAddress) ~ "/" ~ to!string(maskBits); 93 | } 94 | 95 | int maskBits() { 96 | import core.bitop; 97 | if(netmask == 0) 98 | return 0; 99 | return 32-bsf(netmask); 100 | } 101 | 102 | int numberOfAddresses() { 103 | return ~netmask + 1; 104 | } 105 | 106 | uint startingAddress; 107 | uint netmask; 108 | 109 | uint currentAddress; 110 | int remaining; 111 | } 112 | 113 | version(none) 114 | void main() { 115 | // make one with cidr or address + mask notation 116 | 117 | // auto i = IPv4Block("192.168.1.0", "255.255.255.0"); 118 | auto i = IPv4Block("192.168.1.50/29"); 119 | 120 | // loop over all addresses in the block 121 | import std.stdio; 122 | foreach(addr; i) 123 | writeln(addr); 124 | 125 | // show info about the block too 126 | writefln("%s netmask %s", uintToAddress(i.startingAddress), uintToAddress(i.netmask)); 127 | writeln(i); 128 | writeln(i.numberOfAddresses, " addresses in block"); 129 | } 130 | -------------------------------------------------------------------------------- /conv.d: -------------------------------------------------------------------------------- 1 | /++ 2 | A simplified version of `std.conv` with better error messages and faster compiles for supported types. 3 | 4 | History: 5 | Added May 22, 2025 6 | +/ 7 | module arsd.conv; 8 | 9 | static import arsd.core; 10 | 11 | // FIXME: thousands separator for int to string (and float to string) 12 | // FIXME: intToStringArgs 13 | // FIXME: floatToStringArgs 14 | 15 | /++ 16 | Converts a string into the other given type. Throws on failure. 17 | +/ 18 | T to(T)(scope const(char)[] str) { 19 | static if(is(T == enum)) { 20 | switch(str) { 21 | default: 22 | throw new EnumConvException(T.stringof, str.idup); 23 | foreach(memberName; __traits(allMembers, T)) 24 | case memberName: 25 | return __traits(getMember, T, memberName); 26 | } 27 | 28 | } 29 | else 30 | static if(is(T : long)) { 31 | // FIXME: unsigned? overflowing? radix? keep reading or stop on invalid char? 32 | StringToIntArgs args; 33 | args.unsigned = __traits(isUnsigned, T); 34 | long v = stringToInt(str, args); 35 | T ret = cast(T) v; 36 | if(ret != v) 37 | throw new StringToIntConvException("overflow", 0, str.idup, 0); 38 | return ret; 39 | } 40 | else 41 | static if(is(T : double)) { 42 | import core.stdc.stdlib; 43 | import core.stdc.errno; 44 | arsd.core.CharzBuffer z = str; 45 | char* end; 46 | errno = 0; 47 | double res = strtod(z.ptr, &end); 48 | if(end !is (z.ptr + z.length) || errno) { 49 | string msg = errno == ERANGE ? "Over/underflow" : "Invalid input"; 50 | throw new StringToIntConvException(msg, 10, str.idup, end - z.ptr); 51 | } 52 | 53 | return res; 54 | } 55 | else 56 | { 57 | static assert(0, "Unsupported type: " ~ T.stringof); 58 | } 59 | } 60 | 61 | /++ 62 | Converts any given value to a string. The format of the string is unspecified; it is meant for a human reader and might be overridden by types. 63 | +/ 64 | string to(T:string, From)(From value) { 65 | static if(is(From == enum)) 66 | return arsd.core.enumNameForValue(value); 67 | else 68 | return arsd.core.toStringInternal(value); 69 | } 70 | 71 | /++ 72 | Converts ints to other types of ints or enums 73 | +/ 74 | T to(T)(long value) { 75 | static if(is(T == enum)) 76 | return cast(T) value; // FIXME check if the value is actually in range 77 | else 78 | return checkedConversion!T(value); 79 | } 80 | 81 | /+ 82 | T to(T, F)(F value) if(!is(F : const(char)[])) { 83 | // if the language allows implicit conversion, let it do its thing 84 | static if(is(T : F)) { 85 | return value; 86 | } 87 | else 88 | // integral type conversions do checked things 89 | static if(is(T : long) && is(F : long)) { 90 | return checkedConversion!T(value); 91 | } 92 | else 93 | // array to array conversion: try to convert the individual elements, allocating a new return value. 94 | static if(is(T : TE[], TE) && is(F : FE[], FE)) { 95 | F ret = new F(value.length); 96 | foreach(i, e; value) 97 | ret[i] = to!TE(e); 98 | return ret; 99 | } 100 | else 101 | static assert(0, "Unsupported conversion types"); 102 | } 103 | +/ 104 | 105 | unittest { 106 | assert(to!int("5") == 5); 107 | assert(to!int("35") == 35); 108 | assert(to!string(35) == "35"); 109 | assert(to!int("0xA35d") == 0xA35d); 110 | assert(to!int("0b11001001") == 0b11001001); 111 | assert(to!int("0o777") == 511 /*0o777*/); 112 | 113 | assert(to!ubyte("255") == 255); 114 | assert(to!ulong("18446744073709551615") == ulong.max); 115 | 116 | void expectedToThrow(T...)(lazy T items) { 117 | int count; 118 | string messages; 119 | static foreach(idx, item; items) { 120 | try { 121 | auto result = item; 122 | if(messages.length) 123 | messages ~= ","; 124 | messages ~= idx.stringof[0..$-2]; 125 | } catch(StringToIntConvException e) { 126 | // passed the test; it was supposed to throw. 127 | // arsd.core.writeln(e); 128 | count++; 129 | } 130 | } 131 | 132 | assert(count == T.length, "Arg(s) " ~ messages ~ " did not throw"); 133 | } 134 | 135 | expectedToThrow( 136 | to!uint("-44"), // negative number to unsigned reuslt 137 | to!int("add"), // invalid base 10 chars 138 | to!byte("129"), // wrapped to negative 139 | to!int("0p4a0"), // invalid radix prefix 140 | to!int("5000000000"), // doesn't fit in int 141 | to!ulong("6000000000000000000900"), // overflow when reading into the ulong buffer 142 | ); 143 | } 144 | 145 | /++ 146 | 147 | +/ 148 | class ConvException : arsd.core.ArsdExceptionBase { 149 | this(string msg, string file, size_t line) { 150 | super(msg, file, line); 151 | } 152 | } 153 | 154 | /++ 155 | 156 | +/ 157 | class ValueOutOfRangeException : ConvException { 158 | this(string type, long userSuppliedValue, long minimumAcceptableValue, long maximumAcceptableValue, string file = __FILE__, size_t line = __LINE__) { 159 | this.type = type; 160 | this.userSuppliedValue = userSuppliedValue; 161 | this.minimumAcceptableValue = minimumAcceptableValue; 162 | this.maximumAcceptableValue = maximumAcceptableValue; 163 | super("Value was out of range", file, line); 164 | } 165 | 166 | string type; 167 | long userSuppliedValue; 168 | long minimumAcceptableValue; 169 | long maximumAcceptableValue; 170 | 171 | override void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const { 172 | sink("type", type); 173 | sink("userSuppliedValue", arsd.core.toStringInternal(userSuppliedValue)); 174 | sink("minimumAcceptableValue", arsd.core.toStringInternal(minimumAcceptableValue)); 175 | sink("maximumAcceptableValue", arsd.core.toStringInternal(maximumAcceptableValue)); 176 | } 177 | } 178 | 179 | /++ 180 | 181 | +/ 182 | class EnumConvException : ConvException { 183 | this(string type, string userSuppliedValue, string file = __FILE__, size_t line = __LINE__) { 184 | this.type = type; 185 | this.userSuppliedValue = userSuppliedValue; 186 | 187 | super("No such enum value", file, line); 188 | 189 | } 190 | string type; 191 | string userSuppliedValue; 192 | 193 | override void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const { 194 | sink("type", type); 195 | sink("userSuppliedValue", userSuppliedValue); 196 | } 197 | } 198 | 199 | unittest { 200 | enum A { a, b, c } 201 | // to!A("d"); 202 | } 203 | 204 | 205 | /++ 206 | 207 | +/ 208 | class StringToIntConvException : arsd.core.ArsdExceptionBase /*InvalidDataException*/ { 209 | this(string msg, int radix, string userInput, size_t offset, string file = __FILE__, size_t line = __LINE__) { 210 | this.radix = radix; 211 | this.userInput = userInput; 212 | this.offset = offset; 213 | 214 | super(msg, file, line); 215 | } 216 | 217 | override void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const { 218 | sink("radix", arsd.core.toStringInternal(radix)); 219 | sink("userInput", arsd.core.toStringInternal(userInput)); 220 | if(offset < userInput.length) 221 | sink("offset", arsd.core.toStringInternal(offset) ~ " ('" ~ userInput[offset] ~ "')"); 222 | 223 | } 224 | 225 | /// 226 | int radix; 227 | /// 228 | string userInput; 229 | /// 230 | size_t offset; 231 | } 232 | 233 | /++ 234 | if radix is 0, guess from 0o, 0x, 0b prefixes. 235 | +/ 236 | long stringToInt(scope const(char)[] str, StringToIntArgs args = StringToIntArgs.init) { 237 | long accumulator; 238 | 239 | auto original = str; 240 | 241 | Exception exception(string msg, size_t loopOffset = 0, string file = __FILE__, size_t line = __LINE__) { 242 | return new StringToIntConvException(msg, args.radix, original.dup, loopOffset + str.ptr - original.ptr, file, line); 243 | } 244 | 245 | if(str.length == 0) 246 | throw exception("empty string"); 247 | 248 | bool isNegative; 249 | if(str[0] == '-') { 250 | if(args.unsigned) 251 | throw exception("negative number given, but unsigned result desired"); 252 | 253 | isNegative = true; 254 | str = str[1 .. $]; 255 | } 256 | 257 | if(str.length == 0) 258 | throw exception("just a dash"); 259 | 260 | if(str[0] == '0') { 261 | if(str.length > 1 && (str[1] == 'b' || str[1] == 'x' || str[1] == 'o')) { 262 | if(args.radix != 0) { 263 | throw exception("string had specified base, but the radix arg was already supplied"); 264 | } 265 | 266 | switch(str[1]) { 267 | case 'b': 268 | args.radix = 2; 269 | break; 270 | case 'o': 271 | args.radix = 8; 272 | break; 273 | case 'x': 274 | args.radix = 16; 275 | break; 276 | default: 277 | assert(0); 278 | } 279 | 280 | str = str[2 .. $]; 281 | 282 | if(str.length == 0) 283 | throw exception("just a prefix"); 284 | } 285 | } 286 | 287 | if(args.radix == 0) 288 | args.radix = 10; 289 | 290 | foreach(idx, char ch; str) { 291 | 292 | if(ch && ch == args.ignoredSeparator) 293 | continue; 294 | 295 | auto before = accumulator; 296 | 297 | accumulator *= args.radix; 298 | 299 | int value = -1; 300 | if(ch >= '0' && ch <= '9') { 301 | value = ch - '0'; 302 | } else { 303 | ch |= 32; 304 | if(ch >= 'a' && ch <= 'z') 305 | value = ch - 'a' + 10; 306 | } 307 | 308 | if(value < 0) 309 | throw exception("invalid char", idx); 310 | if(value >= args.radix) 311 | throw exception("invalid char for given radix", idx); 312 | 313 | accumulator += value; 314 | if(args.unsigned) { 315 | auto b = cast(ulong) before; 316 | auto a = cast(ulong) accumulator; 317 | if(a < b) 318 | throw exception("value too big to fit in unsigned buffer", idx); 319 | } else { 320 | if(accumulator < before && !args.unsigned) 321 | throw exception("value too big to fit in signed buffer", idx); 322 | } 323 | } 324 | 325 | if(isNegative) 326 | accumulator = -accumulator; 327 | 328 | return accumulator; 329 | } 330 | 331 | /// ditto 332 | struct StringToIntArgs { 333 | int radix; 334 | bool unsigned; 335 | char ignoredSeparator = 0; 336 | } 337 | 338 | /++ 339 | Converts two integer types, returning the min/max of the desired type if the given value is out of range for it. 340 | +/ 341 | T saturatingConversion(T)(long value) { 342 | static assert(is(T : long), "Only works on integer types"); 343 | 344 | static if(is(T == ulong)) // the special case to try to handle the full range there 345 | ulong mv = cast(ulong) value; 346 | else 347 | long mv = value; 348 | 349 | if(mv > T.max) 350 | return T.max; 351 | else if(value < T.min) 352 | return T.min; 353 | else 354 | return cast(T) value; 355 | } 356 | 357 | unittest { 358 | assert(saturatingConversion!ubyte(256) == 255); 359 | assert(saturatingConversion!byte(256) == 127); 360 | assert(saturatingConversion!byte(-256) == -128); 361 | 362 | assert(saturatingConversion!ulong(0) == 0); 363 | assert(saturatingConversion!long(-5) == -5); 364 | 365 | assert(saturatingConversion!uint(-5) == 0); 366 | 367 | // assert(saturatingConversion!ulong(-5) == 0); // it can't catch this since the -5 is indistinguishable from the large ulong value here 368 | } 369 | 370 | /++ 371 | Truncates off bits that won't fit; equivalent to a built-in cast operation (you can just use a cast instead if you want). 372 | +/ 373 | T truncatingConversion(T)(long value) { 374 | static assert(is(T : long), "Only works on integer types"); 375 | 376 | return cast(T) value; 377 | 378 | } 379 | 380 | /++ 381 | Converts two integer types, throwing an exception if the given value is out of range for it. 382 | +/ 383 | T checkedConversion(T)(long value, long minimumAcceptableValue = T.min, long maximumAcceptableValue = T.max) { 384 | static assert(is(T : long), "Only works on integer types"); 385 | 386 | if(value > maximumAcceptableValue) 387 | throw new ValueOutOfRangeException(T.stringof, value, minimumAcceptableValue, maximumAcceptableValue); 388 | else if(value < minimumAcceptableValue) 389 | throw new ValueOutOfRangeException(T.stringof, value, minimumAcceptableValue, maximumAcceptableValue); 390 | else 391 | return cast(T) value; 392 | } 393 | /// ditto 394 | T checkedConversion(T:ulong)(ulong value, ulong minimumAcceptableValue = T.min, ulong maximumAcceptableValue = T.max) { 395 | if(value > maximumAcceptableValue) 396 | throw new ValueOutOfRangeException(T.stringof, value, minimumAcceptableValue, maximumAcceptableValue); 397 | else if(value < minimumAcceptableValue) 398 | throw new ValueOutOfRangeException(T.stringof, value, minimumAcceptableValue, maximumAcceptableValue); 399 | else 400 | return cast(T) value; 401 | } 402 | 403 | unittest { 404 | try { 405 | assert(checkedConversion!byte(155)); 406 | assert(0); 407 | } catch(ValueOutOfRangeException e) { 408 | 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /csv.d: -------------------------------------------------------------------------------- 1 | /// My old csv code. Extremely basic functionality. 2 | module arsd.csv; 3 | 4 | import std.string; 5 | import std.array; 6 | 7 | /// Returns the array of csv rows from the given in-memory data (the argument is NOT a filename). 8 | string[][] readCsv(string data) { 9 | data = data.replace("\r\n", "\n"); 10 | data = data.replace("\r", ""); 11 | 12 | //auto idx = data.indexOf("\n"); 13 | //data = data[idx + 1 .. $]; // skip headers 14 | 15 | string[] fields; 16 | string[][] records; 17 | 18 | string[] current; 19 | 20 | int state = 0; 21 | string field; 22 | foreach(c; data) { 23 | tryit: switch(state) { 24 | default: assert(0); 25 | case 0: // normal 26 | if(c == '"') 27 | state = 1; 28 | else if(c == ',') { 29 | // commit field 30 | current ~= field; 31 | field = null; 32 | } else if(c == '\n') { 33 | // commit record 34 | current ~= field; 35 | 36 | records ~= current; 37 | current = null; 38 | field = null; 39 | } else 40 | field ~= c; 41 | break; 42 | case 1: // in quote 43 | if(c == '"') { 44 | state = 2; 45 | } else 46 | field ~= c; 47 | break; 48 | case 2: // is it a closing quote or an escaped one? 49 | if(c == '"') { 50 | field ~= c; 51 | state = 1; 52 | } else { 53 | state = 0; 54 | goto tryit; 55 | } 56 | } 57 | } 58 | 59 | if(field !is null) 60 | current ~= field; 61 | if(current !is null) 62 | records ~= current; 63 | 64 | 65 | return records; 66 | } 67 | 68 | /// Formats the given rows into csv format. Use like `std.file.write(toCsv(...));` 69 | string toCsv(string[][] rows) { 70 | string data; 71 | 72 | foreach(ridx, row; rows) { 73 | if(ridx) data ~= "\n"; 74 | foreach(idx, cell; row) { 75 | if(idx) data ~= ","; 76 | 77 | if(cell.indexOf(",") != -1 || cell.indexOf("\"") != -1 || cell.indexOf("\n") != -1) { 78 | data ~= "\""; 79 | data ~= cell.replace(`"`, `""`); 80 | data ~= "\""; 81 | } else { 82 | data ~= cell; 83 | } 84 | } 85 | } 86 | 87 | return data; 88 | } 89 | -------------------------------------------------------------------------------- /curl.d: -------------------------------------------------------------------------------- 1 | /// My old curl wrapper. Use [arsd.http2] instead on newer projects, or [std.net.curl] in Phobos if you don't trust my homemade implementations :) 2 | module arsd.curl; 3 | 4 | // see this for info on making a curl.lib on windows: 5 | // http://stackoverflow.com/questions/7933845/where-is-curl-lib-for-dmd 6 | 7 | pragma(lib, "curl"); 8 | 9 | import std.string; 10 | extern(C) { 11 | struct CURL; 12 | struct curl_slist; 13 | 14 | alias int CURLcode; 15 | alias int CURLoption; 16 | 17 | enum int CURLOPT_URL = 10002; 18 | enum int CURLOPT_WRITEFUNCTION = 20011; 19 | enum int CURLOPT_WRITEDATA = 10001; 20 | enum int CURLOPT_POSTFIELDS = 10015; 21 | enum int CURLOPT_POSTFIELDSIZE = 60; 22 | enum int CURLOPT_POST = 47; 23 | enum int CURLOPT_HTTPHEADER = 10023; 24 | enum int CURLOPT_USERPWD = 0x00002715; 25 | 26 | enum int CURLOPT_VERBOSE = 41; 27 | 28 | // enum int CURLOPT_COOKIE = 22; 29 | enum int CURLOPT_COOKIEFILE = 10031; 30 | enum int CURLOPT_COOKIEJAR = 10082; 31 | 32 | enum int CURLOPT_SSL_VERIFYPEER = 64; 33 | 34 | enum int CURLOPT_FOLLOWLOCATION = 52; 35 | 36 | CURL* curl_easy_init(); 37 | void curl_easy_cleanup(CURL* handle); 38 | CURLcode curl_easy_perform(CURL* curl); 39 | 40 | void curl_global_init(int flags); 41 | 42 | enum int CURL_GLOBAL_ALL = 0b1111; 43 | 44 | CURLcode curl_easy_setopt(CURL* handle, CURLoption option, ...); 45 | curl_slist* curl_slist_append(curl_slist*, const char*); 46 | void curl_slist_free_all(curl_slist*); 47 | 48 | // size is size of item, count is how many items 49 | size_t write_data(void* buffer, size_t size, size_t count, void* user) { 50 | string* str = cast(string*) user; 51 | char* data = cast(char*) buffer; 52 | 53 | assert(size == 1); 54 | 55 | *str ~= data[0..count]; 56 | 57 | return count; 58 | } 59 | 60 | char* curl_easy_strerror(CURLcode errornum ); 61 | } 62 | /* 63 | struct CurlOptions { 64 | string username; 65 | string password; 66 | } 67 | */ 68 | 69 | string getDigestString(string s) { 70 | import std.digest.md; 71 | import std.digest; 72 | auto hash = md5Of(s); 73 | auto a = toHexString(hash); 74 | return a.idup; 75 | } 76 | //import std.md5; 77 | import std.file; 78 | /// this automatically caches to a local file for the given time. it ignores the expires header in favor of your time to keep. 79 | version(linux) 80 | string cachedCurl(string url, int maxCacheHours) { 81 | string res; 82 | 83 | auto cacheFile = "/tmp/arsd-curl-cache-" ~ getDigestString(url); 84 | 85 | import std.datetime; 86 | 87 | if(!std.file.exists(cacheFile) || std.file.timeLastModified(cacheFile) < Clock.currTime() - dur!"hours"(maxCacheHours)) { 88 | res = curl(url); 89 | std.file.write(cacheFile, res); 90 | } else { 91 | res = cast(string) std.file.read(cacheFile); 92 | } 93 | 94 | return res; 95 | } 96 | 97 | 98 | string curl(string url, string data = null, string contentType = "application/x-www-form-urlencoded") { 99 | return curlAuth(url, data, null, null, contentType); 100 | } 101 | 102 | string curlCookie(string cookieFile, string url, string data = null, string contentType = "application/x-www-form-urlencoded") { 103 | return curlAuth(url, data, null, null, contentType, null, null, cookieFile); 104 | } 105 | 106 | string curlAuth(string url, string data = null, string username = null, string password = null, string contentType = "application/x-www-form-urlencoded", string methodOverride = null, string[] customHeaders = null, string cookieJar = null) { 107 | CURL* curl = curl_easy_init(); 108 | if(curl is null) 109 | throw new Exception("curl init"); 110 | scope(exit) 111 | curl_easy_cleanup(curl); 112 | 113 | string ret; 114 | 115 | int res; 116 | 117 | debug(arsd_curl_verbose) 118 | curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); 119 | 120 | res = curl_easy_setopt(curl, CURLOPT_URL, std.string.toStringz(url)); 121 | if(res != 0) throw new CurlException(res); 122 | if(username !is null) { 123 | res = curl_easy_setopt(curl, CURLOPT_USERPWD, std.string.toStringz(username ~ ":" ~ password)); 124 | if(res != 0) throw new CurlException(res); 125 | } 126 | res = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &write_data); 127 | if(res != 0) throw new CurlException(res); 128 | res = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret); 129 | if(res != 0) throw new CurlException(res); 130 | 131 | curl_slist* headers = null; 132 | //if(data !is null) 133 | // contentType = ""; 134 | if(contentType.length) 135 | headers = curl_slist_append(headers, toStringz("Content-Type: " ~ contentType)); 136 | 137 | foreach(h; customHeaders) { 138 | headers = curl_slist_append(headers, toStringz(h)); 139 | } 140 | scope(exit) 141 | curl_slist_free_all(headers); 142 | 143 | if(data) { 144 | res = curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.ptr); 145 | if(res != 0) throw new CurlException(res); 146 | res = curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, data.length); 147 | if(res != 0) throw new CurlException(res); 148 | } 149 | 150 | res = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); 151 | if(res != 0) throw new CurlException(res); 152 | 153 | if(cookieJar !is null) { 154 | res = curl_easy_setopt(curl, CURLOPT_COOKIEJAR, toStringz(cookieJar)); 155 | if(res != 0) throw new CurlException(res); 156 | res = curl_easy_setopt(curl, CURLOPT_COOKIEFILE, toStringz(cookieJar)); 157 | if(res != 0) throw new CurlException(res); 158 | } else { 159 | // just want to enable cookie parsing for location 3xx thingies. 160 | // some crappy sites will give you an endless runaround if they can't 161 | // place their fucking tracking cookies. 162 | res = curl_easy_setopt(curl, CURLOPT_COOKIEFILE, toStringz("lol totally not here")); 163 | } 164 | 165 | res = curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); 166 | if(res != 0) throw new CurlException(res); 167 | //res = curl_easy_setopt(curl, 81, 0); // FIXME verify host 168 | //if(res != 0) throw new CurlException(res); 169 | 170 | version(no_curl_follow) {} else { 171 | res = curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); 172 | if(res != 0) throw new CurlException(res); 173 | } 174 | 175 | if(methodOverride !is null) { 176 | switch(methodOverride) { 177 | default: assert(0); 178 | case "POST": 179 | res = curl_easy_setopt(curl, CURLOPT_POST, 1); 180 | break; 181 | case "GET": 182 | //curl_easy_setopt(curl, CURLOPT_POST, 0); 183 | break; 184 | } 185 | } 186 | 187 | auto failure = curl_easy_perform(curl); 188 | if(failure != 0) 189 | throw new CurlException(failure, "\nURL" ~ url); 190 | 191 | return ret; 192 | } 193 | 194 | class CurlException : Exception { 195 | this(CURLcode code, string msg = null, string file = __FILE__, int line = __LINE__) @system { 196 | string message = file ~ ":" ~ to!string(line) ~ " (" ~ to!string(code) ~ ") "; 197 | 198 | auto strerror = curl_easy_strerror(code); 199 | 200 | while(*strerror) { 201 | message ~= *strerror; 202 | strerror++; 203 | } 204 | 205 | super(message ~ msg); 206 | } 207 | } 208 | 209 | 210 | import std.conv; 211 | -------------------------------------------------------------------------------- /declarativeloader.d: -------------------------------------------------------------------------------- 1 | /++ 2 | A declarative file/stream loader/saver. You define structs with a handful of annotations, this read and writes them to/from files. 3 | +/ 4 | module arsd.declarativeloader; 5 | 6 | import std.range; 7 | 8 | /// 9 | enum BigEndian; 10 | /// 11 | enum LittleEndian; 12 | /// @VariableLength indicates the value is saved in a MIDI like format 13 | enum VariableLength; 14 | /// @NumBytes!Field or @NumElements!Field controls length of embedded arrays 15 | struct NumBytes(alias field) {} 16 | /// ditto 17 | struct NumElements(alias field) {} 18 | /// @Tagged!Field indicates a tagged union. Each struct within should have @Tag(X) which is a value of Field 19 | struct Tagged(alias field) {} 20 | /// ditto 21 | auto Tag(T)(T t) { 22 | return TagStruct!T(t); 23 | } 24 | /// For example `@presentIf("version >= 2") int addedInVersion2;` 25 | struct presentIf { string code; } 26 | 27 | 28 | struct TagStruct(T) { T t; } 29 | struct MustBeStruct(T) { T t; } 30 | /// The marked field is not in the actual file 31 | enum NotSaved; 32 | /// Insists the field must be a certain value, like for magic numbers 33 | auto MustBe(T)(T t) { 34 | return MustBeStruct!T(t); 35 | } 36 | 37 | static bool fieldSaved(alias a)() { 38 | bool saved; 39 | static if(is(typeof(a.offsetof))) { 40 | saved = true; 41 | static foreach(attr; __traits(getAttributes, a)) 42 | static if(is(attr == NotSaved)) 43 | saved = false; 44 | } 45 | return saved; 46 | } 47 | 48 | static bool bigEndian(alias a)(bool def) { 49 | bool be = def; 50 | static foreach(attr; __traits(getAttributes, a)) { 51 | static if(is(attr == BigEndian)) 52 | be = true; 53 | else static if(is(attr == LittleEndian)) 54 | be = false; 55 | } 56 | return be; 57 | } 58 | 59 | static auto getTag(alias a)() { 60 | static foreach(attr; __traits(getAttributes, a)) { 61 | static if(is(typeof(attr) == TagStruct!T, T)) { 62 | return attr.t; 63 | } 64 | } 65 | assert(0); 66 | } 67 | 68 | union N(ty) { 69 | ty member; 70 | ubyte[ty.sizeof] bytes; 71 | } 72 | 73 | static bool fieldPresent(alias field, T)(T t) { 74 | bool p = true; 75 | static foreach(attr; __traits(getAttributes, field)) { 76 | static if(is(typeof(attr) == presentIf)) { 77 | bool p2 = false; 78 | with(t) p2 = mixin(attr.code); 79 | p = p && p2; 80 | } 81 | } 82 | return p; 83 | } 84 | 85 | /// input range of ubytes... 86 | int loadFrom(T, Range)(ref T t, auto ref Range r, bool assumeBigEndian = false) { 87 | int bytesConsumed; 88 | string currentItem; 89 | 90 | import std.conv; 91 | try { 92 | 93 | ubyte next() { 94 | if(r.empty) 95 | throw new Exception(T.stringof ~ "." ~ currentItem ~ " trouble " ~ to!string(t)); 96 | auto bfr = r.front; 97 | r.popFront; 98 | bytesConsumed++; 99 | return bfr; 100 | } 101 | 102 | bool endianness = bigEndian!T(assumeBigEndian); 103 | static foreach(memberName; __traits(allMembers, T)) {{ 104 | currentItem = memberName; 105 | static if(is(typeof(__traits(getMember, T, memberName)))) { 106 | alias f = __traits(getMember, T, memberName); 107 | alias ty = typeof(f); 108 | static if(fieldSaved!f) 109 | if(fieldPresent!f(t)) { 110 | endianness = bigEndian!f(endianness); 111 | // FIXME VariableLength 112 | static if(is(ty : ulong) || is(ty : double)) { 113 | N!ty n; 114 | if(endianness) { 115 | foreach(i; 0 .. ty.sizeof) { 116 | version(BigEndian) 117 | n.bytes[i] = next(); 118 | else 119 | n.bytes[$ - 1 - i] = next(); 120 | } 121 | } else { 122 | foreach(i; 0 .. ty.sizeof) { 123 | version(BigEndian) 124 | n.bytes[$ - 1 - i] = next(); 125 | else 126 | n.bytes[i] = next(); 127 | } 128 | } 129 | 130 | // FIXME: MustBe 131 | 132 | __traits(getMember, t, memberName) = n.member; 133 | } else static if(is(ty == struct)) { 134 | bytesConsumed += loadFrom(__traits(getMember, t, memberName), r, endianness); 135 | } else static if(is(ty == union)) { 136 | static foreach(attr; __traits(getAttributes, ty)) 137 | static if(is(attr == Tagged!Field, alias Field)) 138 | enum tagField = __traits(identifier, Field); 139 | static assert(is(typeof(tagField)), "Unions need a Tagged UDA on the union type (not the member) indicating the field that identifies the union"); 140 | 141 | auto tag = __traits(getMember, t, tagField); 142 | // find the child of the union matching the tag... 143 | bool found = false; 144 | static foreach(um; __traits(allMembers, ty)) { 145 | if(tag == getTag!(__traits(getMember, ty, um))) { 146 | bytesConsumed += loadFrom(__traits(getMember, __traits(getMember, t, memberName), um), r, endianness); 147 | found = true; 148 | } 149 | } 150 | if(!found) { 151 | import std.format; 152 | throw new Exception(format("found unknown union tag %s at %s", tag, t)); 153 | } 154 | } else static if(is(ty == E[], E)) { 155 | static foreach(attr; __traits(getAttributes, f)) { 156 | static if(is(attr == NumBytes!Field, alias Field)) 157 | ulong numBytesRemaining = __traits(getMember, t, __traits(identifier, Field)); 158 | else static if(is(attr == NumElements!Field, alias Field)) { 159 | ulong numElementsRemaining = __traits(getMember, t, __traits(identifier, Field)); 160 | } 161 | } 162 | 163 | static if(is(typeof(numBytesRemaining))) { 164 | static if(is(E : const(ubyte)) || is(E : const(char))) { 165 | while(numBytesRemaining) { 166 | __traits(getMember, t, memberName) ~= next; 167 | numBytesRemaining--; 168 | } 169 | } else { 170 | while(numBytesRemaining) { 171 | E piece; 172 | auto by = loadFrom(e, r, endianness); 173 | numBytesRemaining -= by; 174 | bytesConsumed += by; 175 | __traits(getMember, t, memberName) ~= piece; 176 | } 177 | } 178 | } else static if(is(typeof(numElementsRemaining))) { 179 | static if(is(E : const(ubyte)) || is(E : const(char))) { 180 | while(numElementsRemaining) { 181 | __traits(getMember, t, memberName) ~= next; 182 | numElementsRemaining--; 183 | } 184 | } else static if(is(E : const(ushort))) { 185 | while(numElementsRemaining) { 186 | ushort n; 187 | n = next << 8; 188 | n |= next; 189 | // FIXME all of this filth 190 | __traits(getMember, t, memberName) ~= n; 191 | numElementsRemaining--; 192 | } 193 | } else { 194 | while(numElementsRemaining) { 195 | //import std.stdio; writeln(memberName); 196 | E piece; 197 | auto by = loadFrom(piece, r, endianness); 198 | numElementsRemaining--; 199 | 200 | // such a filthy hack, needed for Java's mistake though :( 201 | static if(__traits(compiles, piece.takesTwoSlots())) { 202 | if(piece.takesTwoSlots()) { 203 | __traits(getMember, t, memberName) ~= piece; 204 | numElementsRemaining--; 205 | } 206 | } 207 | 208 | bytesConsumed += by; 209 | __traits(getMember, t, memberName) ~= piece; 210 | } 211 | } 212 | } else static assert(0, "no way to identify length... " ~ memberName); 213 | 214 | } else static assert(0, ty.stringof); 215 | } 216 | } 217 | }} 218 | 219 | } catch(Exception e) { 220 | throw new Exception(T.stringof ~ "." ~ currentItem ~ " trouble " ~ to!string(t), e.file, e.line, e); 221 | } 222 | 223 | return bytesConsumed; 224 | } 225 | 226 | int saveTo(T, Range)(ref T t, ref Range r, bool assumeBigEndian = false) { 227 | int bytesWritten; 228 | string currentItem; 229 | 230 | import std.conv; 231 | try { 232 | 233 | void write(ubyte b) { 234 | bytesWritten++; 235 | static if(is(Range == ubyte[])) 236 | r ~= b; 237 | else 238 | r.put(b); 239 | } 240 | 241 | bool endianness = bigEndian!T(assumeBigEndian); 242 | static foreach(memberName; __traits(allMembers, T)) {{ 243 | currentItem = memberName; 244 | static if(is(typeof(__traits(getMember, T, memberName)))) { 245 | alias f = __traits(getMember, T, memberName); 246 | alias ty = typeof(f); 247 | static if(fieldSaved!f) 248 | if(fieldPresent!f(t)) { 249 | endianness = bigEndian!f(endianness); 250 | // FIXME VariableLength 251 | static if(is(ty : ulong) || is(ty : double)) { 252 | N!ty n; 253 | n.member = __traits(getMember, t, memberName); 254 | if(endianness) { 255 | foreach(i; 0 .. ty.sizeof) { 256 | version(BigEndian) 257 | write(n.bytes[i]); 258 | else 259 | write(n.bytes[$ - 1 - i]); 260 | } 261 | } else { 262 | foreach(i; 0 .. ty.sizeof) { 263 | version(BigEndian) 264 | write(n.bytes[$ - 1 - i]); 265 | else 266 | write(n.bytes[i]); 267 | } 268 | } 269 | 270 | // FIXME: MustBe 271 | } else static if(is(ty == struct)) { 272 | bytesWritten += saveTo(__traits(getMember, t, memberName), r, endianness); 273 | } else static if(is(ty == union)) { 274 | static foreach(attr; __traits(getAttributes, ty)) 275 | static if(is(attr == Tagged!Field, alias Field)) 276 | enum tagField = __traits(identifier, Field); 277 | static assert(is(typeof(tagField)), "Unions need a Tagged UDA on the union type (not the member) indicating the field that identifies the union"); 278 | 279 | auto tag = __traits(getMember, t, tagField); 280 | // find the child of the union matching the tag... 281 | bool found = false; 282 | static foreach(um; __traits(allMembers, ty)) { 283 | if(tag == getTag!(__traits(getMember, ty, um))) { 284 | bytesWritten += saveTo(__traits(getMember, __traits(getMember, t, memberName), um), r, endianness); 285 | found = true; 286 | } 287 | } 288 | if(!found) { 289 | import std.format; 290 | throw new Exception(format("found unknown union tag %s at %s", tag, t)); 291 | } 292 | } else static if(is(ty == E[], E)) { 293 | 294 | // the numBytesRemaining / numElementsRemaining thing here ASSUMING the 295 | // arrays are already the correct size. the struct itself could invariant that maybe 296 | 297 | foreach(item; __traits(getMember, t, memberName)) { 298 | static if(is(typeof(item) == struct)) { 299 | bytesWritten += saveTo(item, r, endianness); 300 | } else { 301 | static struct dummy { 302 | typeof(item) i; 303 | } 304 | dummy d = dummy(item); 305 | bytesWritten += saveTo(d, r, endianness); 306 | } 307 | } 308 | 309 | } else static assert(0, ty.stringof); 310 | } 311 | } 312 | }} 313 | 314 | } catch(Exception e) { 315 | throw new Exception(T.stringof ~ "." ~ currentItem ~ " save trouble " ~ to!string(t), e.file, e.line, e); 316 | } 317 | 318 | return bytesWritten; 319 | } 320 | 321 | unittest { 322 | static struct A { 323 | int a; 324 | @presentIf("a > 5") int b; 325 | int c; 326 | @NumElements!c ubyte[] d; 327 | } 328 | 329 | A a; 330 | a.loadFrom(cast(ubyte[]) [1, 1, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 6, 7, 8]); 331 | 332 | assert(a.a == 257); 333 | assert(a.b == 7); 334 | assert(a.c == 3); 335 | assert(a.d == [6,7,8]); 336 | 337 | a = A.init; 338 | 339 | a.loadFrom(cast(ubyte[]) [0, 0, 0, 0, 7, 0, 0, 0,1,2,3,4,5,6,7]); 340 | assert(a.b == 0); 341 | assert(a.c == 7); 342 | assert(a.d == [1,2,3,4,5,6,7]); 343 | 344 | a.a = 44; 345 | a.c = 3; 346 | a.d = [5,4,3]; 347 | 348 | ubyte[] saved; 349 | 350 | a.saveTo(saved); 351 | 352 | A b; 353 | b.loadFrom(saved); 354 | 355 | assert(a == b); 356 | } 357 | -------------------------------------------------------------------------------- /docs/dev_philosophy.d: -------------------------------------------------------------------------------- 1 | // just docs: Development Philosophy 2 | /++ 3 | This document aims to describe how things work here, what kind of bugs you can expect, how you can ask for new features, and my views on breaking changes and code organization. 4 | 5 | You can read more in my (aspirationally) weekly blog here: http://dpldocs.info/this-week-in-d/Blog.html 6 | 7 | 8 | $(H2 How it is developed) 9 | 10 | This library is mostly developed as a means to an end for me. As such, it might work for you but might not since I usually do just what I need to do to get the job done then move on. I often focus on laying groundwork moreso than "completing" things, since then I can usually add things for myself pretty quickly as I go. 11 | 12 | If you need something, feel free to contact me and I might be able to help code it up fairly quickly and get it to you, then I'll commit later for other people with similar needs. 13 | 14 | $(H2 Code organization) 15 | 16 | The arsd library is more like your public library than most software packages. Like a book library, there's no expectation that every module in here is actually useful to you - you should just browse the available modules and then only use the ones you need. 17 | 18 | Modules usually aim to have as few dependencies as I can, often importing just one or two (or zero!) other modules from the repo. They almost never will import anything from outside this repo or the D standard library. This makes it easy for you to even just download a few individual files from here and use them without the others even being present. 19 | 20 | $(H2 Breaking changes and versioning) 21 | 22 | Typically, once I document something, I try hard to never break it. If I say you can "just download file.d", that's a commitment to never make it import anything else. If I break that commitment, I'll advertise it as a breaking change, add a note to the README file, and bump the package major version on the dub repo, even if nothing else is broken. 23 | 24 | A add things regularly, but until they are documented in a tagged release, I make no promises it won't break. After it is though, I'll maintain it indefinitely (to the extent possible), unless I specifically say otherwise in the documentation comment. 25 | 26 | $(H2 Licensing) 27 | 28 | Everything in here is provided with no warranty of any kind, has no support contract whatsoever, and you assume full responsibility for using it. 29 | 30 | Each file in here may be under a different license, since some of them are adopted ports of other people's code (if I do decide to use a library, I will adopt it and take responsibility for maintaining it myself to ensure our users never have to worry about third-party breakage). Some individual functions will import code with a different license, so you may choose not to use those functions. The documentation will call this out in those specific cases. 31 | 32 | In some cases, different `-version` switches will compile in or version out code with different licenses. Your final result should be assumed to be under the most restrictive license included by any part. I will call this out in the documentation of the modules, if necessary. 33 | 34 | If the documentation and/or source code comments don't say otherwise, you can assume all files are written by me and released under the Boost Software License, 1.0. 35 | 36 | $(WARNING 37 | dub's package format does not necessarily cover the nuance of optional functions. The documentation and copyright notices in the code are authoritative, not the dub package metadata. 38 | ) 39 | 40 | Nothing in this repo will be licensed incompatibly with the GNU Affero GPL. 41 | 42 | $(H2 FAQs) 43 | 44 | $(H3 What does ARSD stand for?) 45 | 46 | Adam Ruppe's Software Development. It was a fake company I made up in my youth and decided to keep the name here as it is generic enough to fit, but not so general it is easily confused with other people's projects. 47 | 48 | $(H3 Why aren't all the modules on dub?) 49 | 50 | dub is not really compatible with my development practices and is bolted on after-the-fact. Since dub requires so much redundant duplication and doesn't benefit me in general, I just haven't done the tedious busy work of filling in its forms for everything. 51 | 52 | I generally accept pull requests though if you want to add one. Use the other subpackages as a template. 53 | 54 | $(H3 Why no arsd/ or source/arsd subdirectories?) 55 | 56 | The repo is $(I already) such a directory. Adding a second one is just a pointless complication. Similarly, the whole directory is source, no need to be redundant, and being redundant actually harms usability. 57 | 58 | If you were to git clone to some directory, all the files here will be placed in their own directory automatically. If you then passed that parent directory as an argument to `dmd -I`, the files in here are found and can be used automatically. You can even clone other repos that use this same layout in there and have all these libraries available, with no complicated setup or build system. 59 | 60 | If I had my way, ALL D projects would use this layout. It makes optional dependencies just work at no cost, C libraries are automatically linked in (thanks to `pragma(lib)`) as-needed, and there's no configuration required. 61 | 62 | $(CONSOLE 63 | $ mkdir libs 64 | $ cd libs 65 | $ git clone git://arsd 66 | $ git clone git://whatever_else 67 | $ dmd -i -I/path/to/libs anything.d # just works! On my computer, I aliased this to `dmdi` 68 | ) 69 | 70 | That's the way I do things on my computer and it works beautifully. Any change now would break that flow without benefiting me at all. 71 | 72 | $(H3 Why are there mixes of spaces and tabs in the code?) 73 | 74 | I use tabs myself, but you can find several parts in the repo with spaces because I do not reject contributions over trivial style differences. 75 | 76 | If I edit contributed code after merging it, I usually keep using the same style as the rest of that function, but sometimes my code editor automatically inserts something else. I don't really care. 77 | +/ 78 | module arsd.docs.dev_philosophy; 79 | -------------------------------------------------------------------------------- /docs/general_concepts.d: -------------------------------------------------------------------------------- 1 | // just docs: General Concepts 2 | /++ 3 | This document describes some general programming concepts and tricks and tips that will make using my APIs easier. 4 | 5 | This document is primarily focused on $(B users) of the library. If you would like to learn more about the $(B implementation) of the library, browser my blog: http://dpldocs.info/this-week-in-d/Blog.html 6 | 7 | 8 | $(H2 Bitmasks) 9 | 10 | See [#bitflags]. 11 | 12 | $(H2 Bitflags) 13 | 14 | Many functions, for example, [arsd.simpledisplay.ScreenPainter.drawText] and [arsd.terminal.Terminal.color], take a `uint` typed argument that is supposed to be made from a combination of `enum` flags defined elsewhere in the file. These are often called "bit flags". 15 | 16 | To create one of these arguments, you use D's bitwise or operator to combine various options. `Color.red | Bright` will combine the values of `Color.red` and `Bright` to make a new argument that [arsd.terminal.Terminal.color] can comprehend. `TextAlignment.Center | TextAlignment.VerticalCenter` makes a combined argument for `drawText`'s `alignment` parameter. 17 | 18 | The `enum` values will have values that go up multiplying by two. If you see values like `1, 2, 4, 8` in an `enum`'s members, there's a good chance it is meant to be combined with the `|` operator when passed to a function. 19 | 20 | The inverse is called a "bit mask" because various bits are "masked" - imagine just seeing someone's eyes through a mask but not their nose - out by the function to deconstruct the combined result back into its individual pieces for processing. D's `&` operator, bitwise and, is used inside the functions to undo the result of `|` on the outside. You can do this too if a function returns a combined result like this. [arsd.simpledisplay.MouseEvent.modifierState] is an example of a struct member made out of individual bits. If you check `if(event.modifierState & ModifierState.leftButtonDown) {}`, you can check for the individual items. 21 | 22 | $(TIP 23 | You can actually combine `|` and `&` in a check. 24 | 25 | ``` 26 | if(event.modifierState & (ModifierState.leftButtonDown | ModifierState.rightButtonDown)) { 27 | // this will be true if either the left OR right buttons are down 28 | } 29 | ``` 30 | ) 31 | +/ 32 | module arsd.docs.general_concepts; 33 | -------------------------------------------------------------------------------- /docs/package.d: -------------------------------------------------------------------------------- 1 | // just docs: Overviews, concepts, and tutorials 2 | /++ 3 | This section of the documentation has bigger picture documents. 4 | 5 | 6 | +/ 7 | module arsd.docs; 8 | -------------------------------------------------------------------------------- /docx.d: -------------------------------------------------------------------------------- 1 | /++ 2 | Bare minimum support for reading Microsoft Word files. 3 | 4 | History: 5 | Added February 19, 2025 6 | +/ 7 | module arsd.docx; 8 | 9 | import arsd.core; 10 | import arsd.zip; 11 | import arsd.dom; 12 | import arsd.color; 13 | 14 | /++ 15 | 16 | +/ 17 | class DocxFile { 18 | private ZipFile zipFile; 19 | private XmlDocument document; 20 | 21 | /++ 22 | 23 | +/ 24 | this(FilePath file) { 25 | this.zipFile = new ZipFile(file); 26 | 27 | load(); 28 | } 29 | 30 | /// ditto 31 | this(immutable(ubyte)[] rawData) { 32 | this.zipFile = new ZipFile(rawData); 33 | 34 | load(); 35 | } 36 | 37 | /++ 38 | Converts the document to a plain text string that gives you 39 | the jist of the document that you can view in a plain editor. 40 | 41 | Most formatting is stripped out. 42 | +/ 43 | string toPlainText() { 44 | string ret; 45 | foreach(paragraph; document.querySelectorAll("w\\:p")) { 46 | if(ret.length) 47 | ret ~= "\n\n"; 48 | ret ~= paragraph.innerText; 49 | } 50 | return ret; 51 | } 52 | 53 | // FIXME: to RTF, markdown, html, and terminal sequences might also be useful. 54 | 55 | private void load() { 56 | loadXml("word/document.xml", (document) { 57 | this.document = document; 58 | }); 59 | } 60 | 61 | private void loadXml(string filename, scope void delegate(XmlDocument document) handler) { 62 | auto document = new XmlDocument(cast(string) zipFile.getContent(filename)); 63 | handler(document); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /english.d: -------------------------------------------------------------------------------- 1 | /// A few helper functions for manipulating English words. Extremely basic. 2 | module arsd.english; 3 | 4 | /++ 5 | Given a non-one `count` argument, will attempt to return the plural version of `word`. Only handles basic cases. If count is one, simply returns the word. 6 | 7 | I originally wrote this for cases like `You have {n} {messages|plural(n)}` in web templates. 8 | +/ 9 | string plural(int count, string word, string pluralWord = null) { 10 | if(count == 1 || word.length == 0) 11 | return word; // it isn't actually plural 12 | 13 | if(pluralWord !is null) 14 | return pluralWord; 15 | 16 | switch(word[$ - 1]) { 17 | case 's': 18 | case 'a', 'e', 'i', 'o', 'u': 19 | return word ~ "es"; 20 | case 'f': 21 | return word[0 .. $-1] ~ "ves"; 22 | case 'y': 23 | return word[0 .. $-1] ~ "ies"; 24 | default: 25 | return word ~ "s"; 26 | } 27 | } 28 | 29 | /// Given an integer, tries to write out the long form number. For example, -5 becomes "negative five". 30 | string numberToEnglish(long number) { 31 | string word; 32 | if(number == 0) 33 | return "zero"; 34 | 35 | if(number < 0) { 36 | word = "negative"; 37 | number = -number; 38 | } 39 | 40 | while(number) { 41 | if(number < 100) { 42 | if(number < singleWords.length) { 43 | word ~= singleWords[cast(int) number]; 44 | break; 45 | } else { 46 | auto tens = number / 10; 47 | word ~= tensPlaceWords[cast(int) tens]; 48 | number = number % 10; 49 | if(number) 50 | word ~= "-"; 51 | } 52 | } else if(number < 1_000) { 53 | auto hundreds = number / 100; 54 | word ~= onesPlaceWords[cast(int) hundreds] ~ " hundred"; 55 | number = number % 100; 56 | if(number) 57 | word ~= " and "; 58 | } else if(number < 1_000_000) { 59 | auto thousands = number / 1_000; 60 | word ~= numberToEnglish(thousands) ~ " thousand"; 61 | number = number % 1_000; 62 | if(number) 63 | word ~= ", "; 64 | } else if(number < 1_000_000_000) { 65 | auto millions = number / 1_000_000; 66 | word ~= numberToEnglish(millions) ~ " million"; 67 | number = number % 1_000_000; 68 | if(number) 69 | word ~= ", "; 70 | } else if(number < 1_000_000_000_000) { 71 | auto n = number / 1_000_000_000; 72 | word ~= numberToEnglish(n) ~ " billion"; 73 | number = number % 1_000_000_000; 74 | if(number) 75 | word ~= ", "; 76 | } else if(number < 1_000_000_000_000_000) { 77 | auto n = number / 1_000_000_000_000; 78 | word ~= numberToEnglish(n) ~ " trillion"; 79 | number = number % 1_000_000_000_000; 80 | if(number) 81 | word ~= ", "; 82 | } else { 83 | import std.conv; 84 | return to!string(number); 85 | } 86 | } 87 | 88 | return word; 89 | } 90 | 91 | unittest { 92 | assert(numberToEnglish(1) == "one"); 93 | assert(numberToEnglish(5) == "five"); 94 | assert(numberToEnglish(13) == "thirteen"); 95 | assert(numberToEnglish(54) == "fifty-four"); 96 | assert(numberToEnglish(178) == "one hundred and seventy-eight"); 97 | assert(numberToEnglish(592) == "five hundred and ninety-two"); 98 | assert(numberToEnglish(1234) == "one thousand, two hundred and thirty-four"); 99 | assert(numberToEnglish(10234) == "ten thousand, two hundred and thirty-four"); 100 | assert(numberToEnglish(105234) == "one hundred and five thousand, two hundred and thirty-four"); 101 | } 102 | 103 | enum onesPlaceWords = [ 104 | "zero", 105 | "one", 106 | "two", 107 | "three", 108 | "four", 109 | "five", 110 | "six", 111 | "seven", 112 | "eight", 113 | "nine", 114 | ]; 115 | 116 | enum singleWords = onesPlaceWords ~ [ 117 | "ten", 118 | "eleven", 119 | "twelve", 120 | "thirteen", 121 | "fourteen", 122 | "fifteen", 123 | "sixteen", 124 | "seventeen", 125 | "eighteen", 126 | "nineteen", 127 | ]; 128 | 129 | enum tensPlaceWords = [ 130 | null, 131 | "ten", 132 | "twenty", 133 | "thirty", 134 | "forty", 135 | "fifty", 136 | "sixty", 137 | "seventy", 138 | "eighty", 139 | "ninety", 140 | ]; 141 | 142 | /* 143 | void main() { 144 | import std.stdio; 145 | foreach(i; 3433000 ..3433325) 146 | writeln(numberToEnglish(i)); 147 | } 148 | */ 149 | -------------------------------------------------------------------------------- /exception.d: -------------------------------------------------------------------------------- 1 | /++ 2 | A draft of a better way to do exceptions 3 | 4 | History: 5 | Originally written in May 2015 as a demo, but I never used it inside arsd. 6 | 7 | Deprecated in March 2023 (dub v11.0), with the successful parts moved to [arsd.core]. It is unlikely to get any future updates. 8 | +/ 9 | deprecated("This was just a proof of concept demo, the actual concepts are now implemented inside arsd.core") 10 | module arsd.exception; 11 | /* 12 | Exceptions 2.0 13 | */ 14 | 15 | interface ThrowableBase { 16 | void fly(string file = __FILE__, size_t line = __LINE__); // should be built into the compiler's throw statement 17 | 18 | // override these as needed 19 | void printMembers(scope void delegate(in char[]) sink) const; // Tip: use mixin PrintMembers; instead of doing it yourself 20 | void getHumanReadableMessage(scope void delegate(in char[]) sink) const; // the exception name should give this generally but it is nice if you have an error code that needs translation or something else that isn't obvious from the name 21 | void printName(scope void delegate(in char[]) sink) const; // only need to override this if you aren't happy with RTTI's name field 22 | 23 | // just call this when you are ready 24 | void toString(scope void delegate(in char[]) sink) const; 25 | } 26 | 27 | mixin template ThrowableBaseImplementation() { 28 | // This sets file and line at the throw point instead of in the ctor 29 | // thereby separating allocation from error information - call this and 30 | // file+line will be set then allowing you to reuse exception objects easier 31 | void fly(string file = __FILE__, size_t line = __LINE__) { 32 | this.file = file; 33 | this.line = line; 34 | throw this; 35 | } 36 | 37 | // You don't really need this - the class name and members should give all the 38 | // necessary info, but it can be nice in cases like a Windows or errno exception 39 | // where the code isn't necessarily as at-a-glance easy as the string from GetLastError. 40 | /* virtual */ void getHumanReadableMessage(scope void delegate(in char[]) sink) const { 41 | sink(msg); // for backward compatibility 42 | } 43 | 44 | // This prints the really useful info to the user, the members' values. 45 | // You don't have to write this typically, instead use the mixin below. 46 | /* virtual */ void printMembers(scope void delegate(in char[]) sink) const { 47 | // this is done with the mixin from derived classes 48 | } 49 | 50 | /* virtual */ void printName(scope void delegate(in char[]) sink) const { 51 | sink(typeid(this).name); // FIXME: would be nice if eponymous templates didn't spew the name twice 52 | } 53 | 54 | override void toString(scope void delegate(in char[]) sink) const { 55 | char[32] tmpBuff = void; 56 | printName(sink); 57 | sink("@"); sink(file); 58 | sink("("); sink(line.sizeToTempString(tmpBuff[])); sink(")"); 59 | sink(": "); getHumanReadableMessage(sink); 60 | sink("\n"); 61 | printMembers(sink); 62 | if (info) { 63 | try { 64 | sink("----------------"); 65 | foreach (t; info) { 66 | sink("\n"); sink(t); 67 | } 68 | } 69 | catch (Throwable) { 70 | // ignore more errors 71 | } 72 | } 73 | } 74 | 75 | } 76 | 77 | class ExceptionBase : Exception, ThrowableBase { 78 | // Hugely simplified ctor - nothing is even needed 79 | this() { 80 | super(""); 81 | } 82 | 83 | mixin ThrowableBaseImplementation; 84 | } 85 | 86 | class ErrorBase : Error, ThrowableBase { 87 | this() { super(""); } 88 | mixin ThrowableBaseImplementation; 89 | } 90 | 91 | // Mix this into your derived class to print all its members automatically for easier debugging! 92 | mixin template PrintMembers() { 93 | override void printMembers(scope void delegate(in char[]) sink) const { 94 | foreach(memberName; __traits(derivedMembers, typeof(this))) { 95 | static if(is(typeof(__traits(getMember, this, memberName))) && !is(typeof(__traits(getMember, typeof(this), memberName)) == function)) { 96 | sink("\t"); 97 | sink(memberName); 98 | sink(" = "); 99 | static if(is(typeof(__traits(getMember, this, memberName)) : const(char)[])) 100 | sink(__traits(getMember, this, memberName)); 101 | else static if(is(typeof(__traits(getMember, this, memberName)) : long)) { 102 | char[32] tmpBuff = void; 103 | sink(sizeToTempString(__traits(getMember, this, memberName), tmpBuff)); 104 | } // else pragma(msg, typeof(__traits(getMember, this, memberName))); 105 | sink("\n"); 106 | } 107 | } 108 | 109 | super.printMembers(sink); 110 | } 111 | } 112 | 113 | // The class name SHOULD obviate this but you can also add another message if you like. 114 | // You can also just override the getHumanReadableMessage yourself in cases like calling strerror 115 | mixin template StaticHumanReadableMessage(string s) { 116 | override void getHumanReadableMessage(scope void delegate(in char[]) sink) const { 117 | sink(s); 118 | } 119 | } 120 | 121 | 122 | 123 | 124 | /* 125 | Enforce 2.0 126 | */ 127 | 128 | interface DynamicException { 129 | /* 130 | TypeInfo getArgumentType(size_t idx); 131 | void* getArgumentData(size_t idx); 132 | string getArgumentAsString(size_t idx); 133 | */ 134 | } 135 | 136 | template enforceBase(ExceptionBaseClass, string failureCondition = "ret is null") { 137 | auto enforceBase(alias func, string file = __FILE__, size_t line = __LINE__, T...)(T args) { 138 | auto ret = func(args); 139 | if(mixin(failureCondition)) { 140 | class C : ExceptionBaseClass, DynamicException { 141 | T args; 142 | this(T args) { 143 | this.args = args; 144 | } 145 | 146 | override void printMembers(scope void delegate(in char[]) sink) const { 147 | import std.traits; 148 | import std.conv; 149 | foreach(idx, arg; args) { 150 | sink("\t"); 151 | sink(ParameterIdentifierTuple!func[idx]); 152 | sink(" = "); 153 | sink(to!string(arg)); 154 | sink("\n"); 155 | } 156 | sink("\treturn value = "); 157 | sink(to!string(ret)); 158 | sink("\n"); 159 | } 160 | 161 | override void printName(scope void delegate(in char[]) sink) const { 162 | sink(__traits(identifier, ExceptionBaseClass)); 163 | } 164 | 165 | override void getHumanReadableMessage(scope void delegate(in char[]) sink) const { 166 | sink(__traits(identifier, func)); 167 | sink(" call failed"); 168 | } 169 | } 170 | 171 | auto exception = new C(args); 172 | exception.file = file; 173 | exception.line = line; 174 | throw exception; 175 | } 176 | 177 | return ret; 178 | } 179 | } 180 | 181 | /// Raises an exception given a set of local variables to print out 182 | void raise(ExceptionBaseClass, T...)(string file = __FILE__, size_t line = __LINE__) { 183 | class C : ExceptionBaseClass, DynamicException { 184 | override void printMembers(scope void delegate(in char[]) sink) const { 185 | import std.conv; 186 | foreach(idx, arg; T) { 187 | sink("\t"); 188 | sink(__traits(identifier, T[idx])); 189 | sink(" = "); 190 | sink(to!string(arg)); 191 | sink("\n"); 192 | } 193 | } 194 | 195 | override void printName(scope void delegate(in char[]) sink) const { 196 | sink(__traits(identifier, ExceptionBaseClass)); 197 | } 198 | } 199 | 200 | auto exception = new C(); 201 | exception.file = file; 202 | exception.line = line; 203 | throw exception; 204 | } 205 | 206 | const(char)[] sizeToTempString(long size, char[] buffer) { 207 | size_t pos = buffer.length - 1; 208 | bool negative = size < 0; 209 | if(size < 0) 210 | size = -size; 211 | while(size) { 212 | buffer[pos] = size % 10 + '0'; 213 | size /= 10; 214 | pos--; 215 | } 216 | if(negative) { 217 | buffer[pos] = '-'; 218 | pos--; 219 | } 220 | return buffer[pos + 1 .. $]; 221 | } 222 | 223 | ///////////////////////////// 224 | /* USAGE EXAMPLE FOLLOWS */ 225 | ///////////////////////////// 226 | 227 | 228 | // Make sure there's sane base classes for things that take 229 | // various types. For example, RangeError might be thrown for 230 | // any type of index, but we might just catch any kind of range error. 231 | // 232 | // The base class gives us an easy catch point for the category. 233 | class MyRangeError : ErrorBase { 234 | // unnecessary but kinda nice to have static error message 235 | mixin StaticHumanReadableMessage!"Index out of bounds"; 236 | } 237 | 238 | // Now, we do a new class for each error condition that can happen 239 | // inheriting from a convenient catch-all base class for our error type 240 | // (which might be ExceptionBase itself btw) 241 | class TypedRangeError(T) : MyRangeError { 242 | // Error details are stored as DATA MEMBERS 243 | // do NOT convert them to a string yourself 244 | this(T index) { 245 | this.index = index; 246 | } 247 | 248 | mixin StaticHumanReadableMessage!(T.stringof ~ " index out of bounds"); 249 | 250 | // The data members can be easily inspected to learn more 251 | // about the error, perhaps even to retry it programmatically 252 | // and this also avoids the need to do something like call to!string 253 | // and string concatenation functions at the construction point. 254 | // 255 | // Yea, this gives more info AND is allocation-free. What's not to love? 256 | // 257 | // Templated ones can be a pain just because of the need to specify it to 258 | // catch or cast, but it will always at least be available in the printed string. 259 | T index; 260 | 261 | // Then, mixin PrintMembers uses D's reflection to do all the messy toString 262 | // data sink nonsense for you. Do this in each subclass where you add more 263 | // data members (which out to be generally all of them, more info is good. 264 | mixin PrintMembers; 265 | } 266 | 267 | version(exception_2_example) { 268 | 269 | // We can pass pre-constructed exceptions to functions and get good file/line and stacktrace info! 270 | void stackExample(ThrowableBase exception) { 271 | // throw it now (custom function cuz I change the behavior a wee bit) 272 | exception.fly(); // ideally, I'd change the throw statement to call this function for you to set up line and file 273 | } 274 | 275 | void main() { 276 | int a = 230; 277 | string file = "lol"; 278 | static class BadValues : ExceptionBase {} 279 | //raise!(BadValues, a, file); 280 | 281 | alias enforce = enforceBase!ExceptionBase; 282 | 283 | import core.stdc.stdio; 284 | auto fp = enforce!fopen("nofile.txt".ptr, "rb".ptr); 285 | 286 | 287 | // construct, passing it error details as data, not strings. 288 | auto exception = new TypedRangeError!int(4); // exception construction is separated from file/line setting 289 | stackExample(exception); // so you can allocate/construct in one place, then set and throw somewhere else 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /file.d: -------------------------------------------------------------------------------- 1 | module arsd.file; 2 | 3 | import arsd.core; 4 | 5 | // file name stuff 6 | alias FilePath = arsd.core.FilePath; 7 | 8 | // the basics 9 | alias writeFile = arsd.core.writeFile; 10 | alias readTextFile = arsd.core.readTextFile; 11 | alias readBinaryFile = arsd.core.readBinaryFile; 12 | 13 | // read lines 14 | 15 | // directory listing 16 | alias getFiles = arsd.core.getFiles; 17 | alias DirectoryWatcher = arsd.core.DirectoryWatcher; 18 | // stat? 19 | // exists? 20 | // symlink? 21 | // remove? 22 | // rename? 23 | // copy? 24 | 25 | // file objects 26 | // alias AsyncFile = arsd.core.AsyncFile; 27 | 28 | /+ 29 | unittest { 30 | writeFile("sample.txt", "this is a test file"); 31 | assert(readTextFile("sample.txt") == "this is a test file"); 32 | } 33 | +/ 34 | -------------------------------------------------------------------------------- /hmac.d: -------------------------------------------------------------------------------- 1 | /// Implementation of the HMAC algorithm. Do not use on new things because [std.digest.hmac] now exists in Phobos. 2 | module arsd.hmac; 3 | 4 | // FIXME: the blocksize is correct for MD5, SHA1, and SHA256 but not generally 5 | // it should really be gotten from the hash 6 | /// 7 | auto hmac(alias hash, size_t blocksize = 64)(in void[] keyv, in void[] messagev) { 8 | 9 | const(ubyte)[] key = cast(const(ubyte)[]) keyv; 10 | const(ubyte)[] message = cast(const(ubyte)[]) messagev; 11 | 12 | if(key.length > blocksize) 13 | key = hash(key); 14 | while(key.length < blocksize) 15 | key ~= 0; 16 | 17 | ubyte[blocksize] o_key_pad; 18 | ubyte[blocksize] i_key_pad; 19 | 20 | foreach(i; 0 .. blocksize) { 21 | o_key_pad[i] = 0x5c ^ key[i]; 22 | i_key_pad[i] = 0x36 ^ key[i]; 23 | } 24 | 25 | return hash(o_key_pad ~ hash(i_key_pad ~ message)); 26 | } 27 | 28 | /* 29 | unittest { 30 | import arsd.sha; 31 | import std.digest.md; 32 | import std.stdio; 33 | writeln(hashToString(hmac!md5Of("", ""))); // 0x74e6f7298a9c2d168935f58c001bad88 34 | 35 | 36 | writeln(hashToString(hmac!md5Of("key", "The quick brown fox jumps over the lazy dog"))); // 0x80070713463e7749b90c2dc24911e275 37 | } 38 | void main(){} 39 | */ 40 | -------------------------------------------------------------------------------- /ico.d: -------------------------------------------------------------------------------- 1 | /++ 2 | Load and save support for Windows .ico icon files. It also supports .cur files, but I've not actually tested them yet. 3 | 4 | History: 5 | Written July 21, 2022 (dub v10.9) 6 | 7 | Save support added April 21, 2023 (dub v11.0) 8 | 9 | Examples: 10 | 11 | --- 12 | void main() { 13 | auto thing = loadIco("test.ico"); 14 | import std.stdio; 15 | writeln(thing.length); // tell how many things it found 16 | 17 | /+ // just to display one 18 | import arsd.simpledisplay; 19 | auto img = new SimpleWindow(thing[0].width, thing[0].height); 20 | { 21 | auto paint = img.draw(); 22 | paint.drawImage(Point(0, 0), Image.fromMemoryImage(thing[0])); 23 | } 24 | 25 | img.eventLoop(0); 26 | +/ 27 | 28 | // and this converts all its versions 29 | import arsd.png; 30 | import std.format; 31 | foreach(idx, t; thing) 32 | writePng(format("test-converted-%d-%dx%d.png", idx, t.width, t.height), t); 33 | } 34 | --- 35 | +/ 36 | module arsd.ico; 37 | 38 | import arsd.png; 39 | import arsd.bmp; 40 | 41 | /++ 42 | A representation of a cursor image as found in a .cur file. 43 | 44 | History: 45 | Added April 21, 2023 (dub v11.0) 46 | +/ 47 | struct IcoCursor { 48 | MemoryImage image; 49 | int hotspotX; 50 | int hotspotY; 51 | } 52 | 53 | /++ 54 | The header of a .ico or .cur file. Note the alignment is $(I not) correct for slurping the file. 55 | +/ 56 | struct IcoHeader { 57 | ushort reserved; 58 | ushort imageType; // 1 = icon, 2 = cursor 59 | ushort numberOfImages; 60 | } 61 | 62 | /++ 63 | The icon directory entry of a .ico or .cur file. Note the alignment is $(I not) correct for slurping the file. 64 | +/ 65 | struct ICONDIRENTRY { 66 | ubyte width; // 0 == 256 67 | ubyte height; // 0 == 256 68 | ubyte numColors; // 0 == no palette 69 | ubyte reserved; 70 | ushort planesOrHotspotX; 71 | ushort bppOrHotspotY; 72 | uint imageDataSize; 73 | uint imageDataOffset; // from beginning of file 74 | } 75 | 76 | // the file goes header, then array of dir entries, then images 77 | /* 78 | Recall that if an image is stored in BMP format, it must exclude the opening BITMAPFILEHEADER structure, whereas if it is stored in PNG format, it must be stored in its entirety. 79 | 80 | Note that the height of the BMP image must be twice the height declared in the image directory. The second half of the bitmap should be an AND mask for the existing screen pixels, with the output pixels given by the formula Output = (Existing AND Mask) XOR Image. Set the mask to be zero everywhere for a clean overwrite. 81 | 82 | from wikipedia 83 | */ 84 | 85 | /++ 86 | Loads a ico file off the given file or from the given memory block. 87 | 88 | Returns: 89 | Array of individual images found in the icon file. These are typically different size representations of the same icon. 90 | +/ 91 | MemoryImage[] loadIco(string filename) { 92 | import std.file; 93 | return loadIcoFromMemory(cast(const(ubyte)[]) std.file.read(filename)); 94 | } 95 | 96 | /// ditto 97 | MemoryImage[] loadIcoFromMemory(const(ubyte)[] data) { 98 | MemoryImage[] images; 99 | int spot; 100 | loadIcoOrCurFromMemoryCallback( 101 | data, 102 | (int imageType, int numberOfImages) { 103 | if(imageType > 1) 104 | throw new Exception("Not an icon file - invalid image type header"); 105 | 106 | images.length = numberOfImages; 107 | }, 108 | (MemoryImage mi, int hotspotX, int hotspotY) { 109 | images[spot++] = mi; 110 | } 111 | ); 112 | 113 | assert(spot == images.length); 114 | 115 | return images; 116 | } 117 | 118 | /++ 119 | Loads a .cur file. 120 | 121 | History: 122 | Added April 21, 2023 (dub v11.0) 123 | +/ 124 | IcoCursor[] loadCurFromMemory(const(ubyte)[] data) { 125 | IcoCursor[] images; 126 | int spot; 127 | loadIcoOrCurFromMemoryCallback( 128 | data, 129 | (int imageType, int numberOfImages) { 130 | if(imageType != 2) 131 | throw new Exception("Not an cursor file - invalid image type header"); 132 | 133 | images.length = numberOfImages; 134 | }, 135 | (MemoryImage mi, int hotspotX, int hotspotY) { 136 | images[spot++] = IcoCursor(mi, hotspotX, hotspotY); 137 | } 138 | ); 139 | 140 | assert(spot == images.length); 141 | 142 | return images; 143 | 144 | } 145 | 146 | /++ 147 | Load implementation. Api subject to change. 148 | +/ 149 | void loadIcoOrCurFromMemoryCallback( 150 | const(ubyte)[] data, 151 | scope void delegate(int imageType, int numberOfImages) imageTypeChecker, 152 | scope void delegate(MemoryImage mi, int hotspotX, int hotspotY) encounteredImage, 153 | ) { 154 | IcoHeader header; 155 | if(data.length < 6) 156 | throw new Exception("Not an icon file - too short to have a header"); 157 | header.reserved |= data[0]; 158 | header.reserved |= data[1] << 8; 159 | 160 | header.imageType |= data[2]; 161 | header.imageType |= data[3] << 8; 162 | 163 | header.numberOfImages |= data[4]; 164 | header.numberOfImages |= data[5] << 8; 165 | 166 | if(header.reserved != 0) 167 | throw new Exception("Not an icon file - first bytes incorrect"); 168 | 169 | imageTypeChecker(header.imageType, header.numberOfImages); 170 | 171 | auto originalData = data; 172 | data = data[6 .. $]; 173 | 174 | ubyte nextByte() { 175 | if(data.length == 0) 176 | throw new Exception("Invalid icon file, it too short"); 177 | ubyte b = data[0]; 178 | data = data[1 .. $]; 179 | return b; 180 | } 181 | 182 | ICONDIRENTRY readDirEntry() { 183 | ICONDIRENTRY ide; 184 | ide.width = nextByte(); 185 | ide.height = nextByte(); 186 | ide.numColors = nextByte(); 187 | ide.reserved = nextByte(); 188 | 189 | ide.planesOrHotspotX |= nextByte(); 190 | ide.planesOrHotspotX |= nextByte() << 8; 191 | 192 | ide.bppOrHotspotY |= nextByte(); 193 | ide.bppOrHotspotY |= nextByte() << 8; 194 | 195 | ide.imageDataSize |= nextByte() << 0; 196 | ide.imageDataSize |= nextByte() << 8; 197 | ide.imageDataSize |= nextByte() << 16; 198 | ide.imageDataSize |= nextByte() << 24; 199 | 200 | ide.imageDataOffset |= nextByte() << 0; 201 | ide.imageDataOffset |= nextByte() << 8; 202 | ide.imageDataOffset |= nextByte() << 16; 203 | ide.imageDataOffset |= nextByte() << 24; 204 | 205 | return ide; 206 | } 207 | 208 | ICONDIRENTRY[] ides; 209 | foreach(i; 0 .. header.numberOfImages) 210 | ides ~= readDirEntry(); 211 | 212 | foreach(image; ides) { 213 | if(image.imageDataOffset >= originalData.length) 214 | throw new Exception("Invalid icon file - image data offset beyond file size"); 215 | if(image.imageDataOffset + image.imageDataSize > originalData.length) 216 | throw new Exception("Invalid icon file - image data extends beyond file size"); 217 | 218 | auto idata = originalData[image.imageDataOffset .. image.imageDataOffset + image.imageDataSize]; 219 | 220 | if(idata.length < 4) 221 | throw new Exception("Invalid image, not long enough to identify"); 222 | 223 | if(idata[0 .. 4] == "\x89PNG") { 224 | encounteredImage(readPngFromBytes(idata), image.planesOrHotspotX, image.bppOrHotspotY); 225 | } else { 226 | encounteredImage(readBmp(idata, false, false, true), image.planesOrHotspotX, image.bppOrHotspotY); 227 | } 228 | } 229 | } 230 | 231 | /++ 232 | History: 233 | Added April 21, 2023 (dub v11.0) 234 | +/ 235 | void writeIco(string filename, MemoryImage[] images) { 236 | writeIcoOrCur(filename, false, cast(int) images.length, (int idx) { return IcoCursor(images[idx]); }); 237 | } 238 | 239 | /// ditto 240 | void writeCur(string filename, IcoCursor[] images) { 241 | writeIcoOrCur(filename, true, cast(int) images.length, (int idx) { return images[idx]; }); 242 | } 243 | 244 | /++ 245 | Save implementation. Api subject to change. 246 | +/ 247 | void writeIcoOrCur(string filename, bool isCursor, int count, scope IcoCursor delegate(int) getImageAndHotspots) { 248 | IcoHeader header; 249 | header.reserved = 0; 250 | header.imageType = isCursor ? 2 : 1; 251 | if(count > ushort.max) 252 | throw new Exception("too many images for icon file"); 253 | header.numberOfImages = cast(ushort) count; 254 | 255 | enum headerSize = 6; 256 | enum dirEntrySize = 16; 257 | 258 | int dataFilePos = headerSize + dirEntrySize * cast(int) count; 259 | 260 | ubyte[][] pngs; 261 | ICONDIRENTRY[] dirEntries; 262 | dirEntries.length = count; 263 | pngs.length = count; 264 | foreach(idx, ref entry; dirEntries) { 265 | auto image = getImageAndHotspots(cast(int) idx); 266 | if(image.image.width > 256 || image.image.height > 256) 267 | throw new Exception("image too big for icon file"); 268 | entry.width = image.image.width == 256 ? 0 : cast(ubyte) image.image.width; 269 | entry.height = image.image.height == 256 ? 0 : cast(ubyte) image.image.height; 270 | 271 | entry.planesOrHotspotX = isCursor ? cast(ushort) image.hotspotX : 0; 272 | entry.bppOrHotspotY = isCursor ? cast(ushort) image.hotspotY : 0; 273 | 274 | auto png = writePngToArray(image.image); 275 | 276 | entry.imageDataSize = cast(uint) png.length; 277 | entry.imageDataOffset = dataFilePos; 278 | dataFilePos += entry.imageDataSize; 279 | 280 | pngs[idx] = png; 281 | } 282 | 283 | ubyte[] data; 284 | data.length = dataFilePos; 285 | int pos = 0; 286 | 287 | data[pos++] = (header.reserved >> 0) & 0xff; 288 | data[pos++] = (header.reserved >> 8) & 0xff; 289 | data[pos++] = (header.imageType >> 0) & 0xff; 290 | data[pos++] = (header.imageType >> 8) & 0xff; 291 | data[pos++] = (header.numberOfImages >> 0) & 0xff; 292 | data[pos++] = (header.numberOfImages >> 8) & 0xff; 293 | 294 | foreach(entry; dirEntries) { 295 | data[pos++] = (entry.width >> 0) & 0xff; 296 | data[pos++] = (entry.height >> 0) & 0xff; 297 | data[pos++] = (entry.numColors >> 0) & 0xff; 298 | data[pos++] = (entry.reserved >> 0) & 0xff; 299 | data[pos++] = (entry.planesOrHotspotX >> 0) & 0xff; 300 | data[pos++] = (entry.planesOrHotspotX >> 8) & 0xff; 301 | data[pos++] = (entry.bppOrHotspotY >> 0) & 0xff; 302 | data[pos++] = (entry.bppOrHotspotY >> 8) & 0xff; 303 | 304 | data[pos++] = (entry.imageDataSize >> 0) & 0xff; 305 | data[pos++] = (entry.imageDataSize >> 8) & 0xff; 306 | data[pos++] = (entry.imageDataSize >> 16) & 0xff; 307 | data[pos++] = (entry.imageDataSize >> 24) & 0xff; 308 | 309 | data[pos++] = (entry.imageDataOffset >> 0) & 0xff; 310 | data[pos++] = (entry.imageDataOffset >> 8) & 0xff; 311 | data[pos++] = (entry.imageDataOffset >> 16) & 0xff; 312 | data[pos++] = (entry.imageDataOffset >> 24) & 0xff; 313 | } 314 | 315 | foreach(png; pngs) { 316 | data[pos .. pos + png.length] = png[]; 317 | pos += png.length; 318 | } 319 | 320 | assert(pos == dataFilePos); 321 | 322 | import std.file; 323 | std.file.write(filename, data); 324 | } 325 | -------------------------------------------------------------------------------- /jpg.d: -------------------------------------------------------------------------------- 1 | /// Reads a jpg header without reading the rest of the file. Use [arsd.jpeg] for an actual image loader if you need actual data, but this is kept around for times when you only care about basic info like image dimensions. 2 | module arsd.jpg; 3 | 4 | import std.typecons; 5 | import std.stdio; 6 | import std.conv; 7 | 8 | struct JpegSection { 9 | ubyte identifier; 10 | ubyte[] data; 11 | } 12 | 13 | // gives as a range of file sections 14 | struct LazyJpegFile { 15 | File f; 16 | JpegSection _front; 17 | bool _frontIsValid; 18 | this(File f) { 19 | this.f = f; 20 | 21 | ubyte[2] headerBuffer; 22 | auto data = f.rawRead(headerBuffer); 23 | if(data != [0xff, 0xd8]) 24 | throw new Exception("no jpeg header"); 25 | popFront(); // prime 26 | } 27 | 28 | void popFront() { 29 | ubyte[4] startingBuffer; 30 | auto read = f.rawRead(startingBuffer); 31 | if(read.length != 4) { 32 | _frontIsValid = false; 33 | return; // end of file 34 | } 35 | 36 | if(startingBuffer[0] != 0xff) 37 | throw new Exception("not lined up in file"); 38 | 39 | _front.identifier = startingBuffer[1]; 40 | ushort length = cast(ushort) (startingBuffer[2]) * 256 + startingBuffer[3]; 41 | 42 | if(length < 2) 43 | throw new Exception("wtf"); 44 | length -= 2; // the length in the file includes the block header, but we just want the data here 45 | 46 | _front.data = new ubyte[](length); 47 | read = f.rawRead(_front.data); 48 | if(read.length != length) 49 | throw new Exception("didn't read the file right, got " ~ to!string(read.length) ~ " instead of " ~ to!string(length)); 50 | 51 | _frontIsValid = true; 52 | } 53 | 54 | JpegSection front() { 55 | return _front; 56 | } 57 | 58 | bool empty() { 59 | return !_frontIsValid; 60 | } 61 | } 62 | 63 | // returns width, height 64 | Tuple!(int, int) getSizeFromFile(string filename) { 65 | import std.stdio; 66 | 67 | auto file = File(filename, "rb"); 68 | 69 | auto jpeg = LazyJpegFile(file); 70 | 71 | auto firstSection = jpeg.front(); 72 | jpeg.popFront(); 73 | 74 | // commented because exif and jfif are both readable by this so no need to be picky 75 | //if(firstSection.identifier != 0xe0) 76 | //throw new Exception("bad header"); 77 | 78 | for(; !jpeg.empty(); jpeg.popFront()) { 79 | if(jpeg.front.identifier != 0xc0) 80 | continue; 81 | auto data = jpeg.front.data[1..$]; // skip the precision byte 82 | 83 | ushort height = data[0] * 256 + data[1]; 84 | ushort width = data[2] * 256 + data[3]; 85 | return tuple(cast(int) width, cast(int) height); 86 | } 87 | 88 | throw new Exception("idk about the length"); 89 | } 90 | 91 | version(with_libjpeg) { 92 | /+ 93 | import arsd.color; 94 | 95 | TrueColorImage read_JPEG_file(string filename) { 96 | /* This struct contains the JPEG decompression parameters and pointers to 97 | * working space (which is allocated as needed by the JPEG library). 98 | */ 99 | struct jpeg_decompress_struct cinfo; 100 | /* We use our private extension JPEG error handler. 101 | * Note that this struct must live as long as the main JPEG parameter 102 | * struct, to avoid dangling-pointer problems. 103 | */ 104 | struct my_error_mgr jerr; 105 | /* More stuff */ 106 | FILE * infile; /* source file */ 107 | JSAMPARRAY buffer; /* Output row buffer */ 108 | int row_stride; /* physical row width in output buffer */ 109 | 110 | /* In this example we want to open the input file before doing anything else, 111 | * so that the setjmp() error recovery below can assume the file is open. 112 | * VERY IMPORTANT: use "b" option to fopen() if you are on a machine that 113 | * requires it in order to read binary files. 114 | */ 115 | 116 | if ((infile = fopen(filename, "rb")) == NULL) { 117 | fprintf(stderr, "can't open %s\n", filename); 118 | return 0; 119 | } 120 | 121 | /* Step 1: allocate and initialize JPEG decompression object */ 122 | 123 | /* We set up the normal JPEG error routines, then override error_exit. */ 124 | cinfo.err = jpeg_std_error(&jerr.pub); 125 | jerr.pub.error_exit = my_error_exit; 126 | /* Establish the setjmp return context for my_error_exit to use. */ 127 | if (setjmp(jerr.setjmp_buffer)) { 128 | /* If we get here, the JPEG code has signaled an error. 129 | * We need to clean up the JPEG object, close the input file, and return. 130 | */ 131 | jpeg_destroy_decompress(&cinfo); 132 | fclose(infile); 133 | return 0; 134 | } 135 | /* Now we can initialize the JPEG decompression object. */ 136 | jpeg_create_decompress(&cinfo); 137 | 138 | /* Step 2: specify data source (eg, a file) */ 139 | 140 | jpeg_stdio_src(&cinfo, infile); 141 | 142 | /* Step 3: read file parameters with jpeg_read_header() */ 143 | 144 | (void) jpeg_read_header(&cinfo, TRUE); 145 | /* We can ignore the return value from jpeg_read_header since 146 | * (a) suspension is not possible with the stdio data source, and 147 | * (b) we passed TRUE to reject a tables-only JPEG file as an error. 148 | * See libjpeg.txt for more info. 149 | */ 150 | 151 | /* Step 4: set parameters for decompression */ 152 | 153 | /* In this example, we don't need to change any of the defaults set by 154 | * jpeg_read_header(), so we do nothing here. 155 | */ 156 | 157 | /* Step 5: Start decompressor */ 158 | 159 | (void) jpeg_start_decompress(&cinfo); 160 | /* We can ignore the return value since suspension is not possible 161 | * with the stdio data source. 162 | */ 163 | 164 | /* We may need to do some setup of our own at this point before reading 165 | * the data. After jpeg_start_decompress() we have the correct scaled 166 | * output image dimensions available, as well as the output colormap 167 | * if we asked for color quantization. 168 | * In this example, we need to make an output work buffer of the right size. 169 | */ 170 | /* JSAMPLEs per row in output buffer */ 171 | row_stride = cinfo.output_width * cinfo.output_components; 172 | /* Make a one-row-high sample array that will go away when done with image */ 173 | buffer = (*cinfo.mem->alloc_sarray) 174 | ((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1); 175 | 176 | /* Step 6: while (scan lines remain to be read) */ 177 | /* jpeg_read_scanlines(...); */ 178 | 179 | /* Here we use the library's state variable cinfo.output_scanline as the 180 | * loop counter, so that we don't have to keep track ourselves. 181 | */ 182 | while (cinfo.output_scanline < cinfo.output_height) { 183 | /* jpeg_read_scanlines expects an array of pointers to scanlines. 184 | * Here the array is only one element long, but you could ask for 185 | * more than one scanline at a time if that's more convenient. 186 | */ 187 | (void) jpeg_read_scanlines(&cinfo, buffer, 1); 188 | /* Assume put_scanline_someplace wants a pointer and sample count. */ 189 | put_scanline_someplace(buffer[0], row_stride); 190 | } 191 | 192 | /* Step 7: Finish decompression */ 193 | 194 | (void) jpeg_finish_decompress(&cinfo); 195 | /* We can ignore the return value since suspension is not possible 196 | * with the stdio data source. 197 | */ 198 | 199 | /* Step 8: Release JPEG decompression object */ 200 | 201 | /* This is an important step since it will release a good deal of memory. */ 202 | jpeg_destroy_decompress(&cinfo); 203 | 204 | /* After finish_decompress, we can close the input file. 205 | * Here we postpone it until after no more JPEG errors are possible, 206 | * so as to simplify the setjmp error logic above. (Actually, I don't 207 | * think that jpeg_destroy can do an error exit, but why assume anything...) 208 | */ 209 | fclose(infile); 210 | 211 | /* At this point you may want to check to see whether any corrupt-data 212 | * warnings occurred (test whether jerr.pub.num_warnings is nonzero). 213 | */ 214 | 215 | /* And we're done! */ 216 | return 1; 217 | } 218 | +/ 219 | } 220 | -------------------------------------------------------------------------------- /libeay32.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamdruppe/arsd/b3d88cafa8d90d70655a56af8f24d05165967248/libeay32.dll -------------------------------------------------------------------------------- /libssh2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamdruppe/arsd/b3d88cafa8d90d70655a56af8f24d05165967248/libssh2.dll -------------------------------------------------------------------------------- /libssh2.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamdruppe/arsd/b3d88cafa8d90d70655a56af8f24d05165967248/libssh2.lib -------------------------------------------------------------------------------- /mailserver.d: -------------------------------------------------------------------------------- 1 | /++ 2 | A bare-bones, dead simple incoming SMTP server with zero outbound mail support. Intended for applications that want to process inbound email on a VM or something. 3 | 4 | 5 | $(H2 Alternatives) 6 | 7 | You can also run a real email server and process messages as they are delivered with a biff notification or get them from imap or something too. 8 | 9 | History: 10 | Written December 26, 2020, in a little over one hour. Don't expect much from it! 11 | +/ 12 | module arsd.mailserver; 13 | 14 | import arsd.fibersocket; 15 | import arsd.email; 16 | 17 | /// 18 | struct SmtpServerConfig { 19 | //string iface = null; 20 | ushort port = 25; 21 | string hostname; 22 | } 23 | 24 | /// 25 | void serveSmtp(FiberManager fm, SmtpServerConfig config, void delegate(string[] recipients, IncomingEmailMessage) handler) { 26 | fm.listenTcp4(config.port, (Socket socket) { 27 | ubyte[512] buffer; 28 | ubyte[] at; 29 | const(ubyte)[] readLine() { 30 | top: 31 | int index = -1; 32 | foreach(idx, b; at) { 33 | if(b == 10) { 34 | index = cast(int) idx; 35 | break; 36 | } 37 | } 38 | if(index != -1) { 39 | auto got = at[0 .. index]; 40 | at = at[index + 1 .. $]; 41 | if(got.length) { 42 | if(got[$-1] == '\n') 43 | got = got[0 .. $-1]; 44 | if(got[$-1] == '\r') 45 | got = got[0 .. $-1]; 46 | } 47 | return got; 48 | } 49 | if(at.ptr is buffer.ptr && at.length < buffer.length) { 50 | auto got = socket.receive(buffer[at.length .. $]); 51 | if(got < 0) { 52 | socket.close(); 53 | return null; 54 | } if(got == 0) { 55 | socket.close(); 56 | return null; 57 | } else { 58 | at = buffer[0 .. at.length + got]; 59 | goto top; 60 | } 61 | } else { 62 | // no space 63 | if(at.ptr is buffer.ptr) 64 | at = at.dup; 65 | 66 | auto got = socket.receive(buffer[]); 67 | if(got <= 0) { 68 | socket.close(); 69 | return null; 70 | } else { 71 | at ~= buffer[0 .. got]; 72 | goto top; 73 | } 74 | } 75 | 76 | assert(0); 77 | } 78 | 79 | socket.sendAll("220 " ~ config.hostname ~ " SMTP arsd_mailserver\r\n"); // ESMTP? 80 | 81 | immutable(ubyte)[][] msgLines; 82 | string[] recipients; 83 | 84 | loop: while(socket.isAlive()) { 85 | auto line = readLine(); 86 | if(line is null) { 87 | socket.close(); 88 | break; 89 | } 90 | 91 | if(line.length < 4) { 92 | socket.sendAll("500 Unknown command"); 93 | continue; 94 | } 95 | 96 | switch(cast(string) line[0 .. 4]) { 97 | case "HELO": 98 | socket.sendAll("250 " ~ config.hostname ~ " Hello, good to see you\r\n"); 99 | break; 100 | case "EHLO": 101 | goto default; // FIXME 102 | case "MAIL": 103 | // MAIL FROM: 104 | // 501 5.1.7 Syntax error in mailbox address "me@a?example.com.arsdnet.net" (non-printable character) 105 | 106 | if(line.length < 11 || line[0 .. 10] != "MAIL FROM:") { 107 | socket.sendAll("501 Syntax error"); 108 | continue; 109 | } 110 | 111 | line = line[10 .. $]; 112 | if(line[0] == '<') { 113 | if(line[$-1] != '>') { 114 | socket.sendAll("501 Syntax error"); 115 | continue; 116 | } 117 | 118 | line = line[1 .. $-1]; 119 | } 120 | 121 | string currentDate; // FIXME 122 | msgLines ~= cast(immutable(ubyte)[]) ("From " ~ cast(string) line ~ " " ~ currentDate); 123 | msgLines ~= cast(immutable(ubyte)[]) ("Received: from " ~ socket.remoteAddress.toString); 124 | 125 | socket.sendAll("250 OK\r\n"); 126 | break; 127 | case "RCPT": 128 | // RCPT TO:<...> 129 | 130 | if(line.length < 9 || line[0 .. 8] != "RCPT TO:") { 131 | socket.sendAll("501 Syntax error"); 132 | continue; 133 | } 134 | 135 | line = line[8 .. $]; 136 | if(line[0] == '<') { 137 | if(line[$-1] != '>') { 138 | socket.sendAll("501 Syntax error"); 139 | continue; 140 | } 141 | 142 | line = line[1 .. $-1]; 143 | } 144 | 145 | recipients ~= (cast(char[]) line).idup; 146 | 147 | socket.sendAll("250 OK\r\n"); 148 | break; 149 | case "DATA": 150 | socket.sendAll("354 Enter mail, end with . on line by itself\r\n"); 151 | 152 | more_lines: 153 | line = readLine(); 154 | 155 | if(line == ".") { 156 | handler(recipients, new IncomingEmailMessage(msgLines)); 157 | socket.sendAll("250 OK\r\n"); 158 | } else if(line is null) { 159 | socket.close(); 160 | break loop; 161 | } else { 162 | msgLines ~= line.idup; 163 | goto more_lines; 164 | } 165 | break; 166 | case "QUIT": 167 | socket.sendAll("221 Bye\r\n"); 168 | socket.close(); 169 | break; 170 | default: 171 | socket.sendAll("500 5.5.1 Command unrecognized\r\n"); 172 | } 173 | } 174 | }); 175 | } 176 | 177 | version(Demo) 178 | void main() { 179 | auto fm = new FiberManager; 180 | 181 | fm.serveSmtp(SmtpServerConfig(9025), (string[] recipients, IncomingEmailMessage iem) { 182 | import std.stdio; 183 | writeln(recipients); 184 | writeln(iem.subject); 185 | writeln(iem.textMessageBody); 186 | }); 187 | 188 | fm.run; 189 | } 190 | -------------------------------------------------------------------------------- /mangle.d: -------------------------------------------------------------------------------- 1 | /// a D name mangler. You probably do not need this, use the language's built in `.mangleof` property instead. I don't even remember why I wrote it. 2 | module arsd.mangle; 3 | 4 | import std.conv; 5 | 6 | static immutable string[23] primitives = [ 7 | "char", // a 8 | "bool", // b 9 | "creal", // c 10 | "double", // d 11 | "real", // e 12 | "float", // f 13 | "byte", // g 14 | "ubyte", // h 15 | "int", // i 16 | "ireal", // j 17 | "uint", // k 18 | "long", // l 19 | "ulong", // m 20 | null, // n 21 | "ifloat", // o 22 | "idouble", // p 23 | "cfloat", // q 24 | "cdouble", // r 25 | "short", // s 26 | "ushort", // t 27 | "wchar", // u 28 | "void", // v 29 | "dchar", // w 30 | ]; 31 | 32 | // FIXME: using this will allocate at *runtime*! Unbelievable. 33 | // it does that even if everything is enum 34 | auto dTokensPain() { 35 | immutable p = cast(immutable(string[])) primitives[]; 36 | string[] ret; 37 | foreach(i; (sort!"a.length > b.length"( 38 | p~ 39 | [ 40 | "(", 41 | ")", 42 | ".", 43 | ",", 44 | "!", 45 | "[", 46 | "]", 47 | "*", 48 | "const", 49 | "immutable", 50 | "shared", 51 | "extern", 52 | ]))) { ret ~= i; } 53 | 54 | return ret; 55 | } 56 | 57 | static immutable string[] dTokens = dTokensPain(); 58 | 59 | 60 | char manglePrimitive(in char[] t) { 61 | foreach(i, p; primitives) 62 | if(p == t) 63 | return cast(char) ('a' + i); 64 | return 0; 65 | } 66 | 67 | import std.algorithm; 68 | import std.array; 69 | 70 | bool isIdentifierChar(char c) { 71 | // FIXME: match the D spec 72 | return c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'); 73 | } 74 | 75 | struct StackArray(Type, size_t capacity) { 76 | Type[capacity] buffer; 77 | size_t length; 78 | Type[] slice() { return buffer[0 .. length]; } 79 | void opOpAssign(string op : "~")(Type rhs, string file = __FILE__, size_t line = __LINE__) { 80 | if(length >= capacity) { 81 | throw new Error("no more room", file, line); 82 | } 83 | buffer[length] = rhs; 84 | length++; 85 | } 86 | } 87 | 88 | char[] mangle(const(char)[] decl, char[] buffer) { 89 | 90 | 91 | StackArray!(const(char)[], 128) tokensBuffer; 92 | main: while(decl.length) { 93 | if(decl[0] == ' ' || decl[0] == '\t' || decl[0] == '\n') { 94 | decl = decl[1 .. $]; 95 | continue; 96 | } 97 | 98 | foreach(token; dTokens) { 99 | if(token is null) continue; 100 | if(decl.length >= token.length && decl[0 .. token.length] == token) { 101 | // make sure this isn't an identifier that coincidentally starts with a keyword 102 | if(decl.length == token.length || !token[$ - 1].isIdentifierChar() || !decl[token.length].isIdentifierChar()) { 103 | tokensBuffer ~= token; 104 | decl = decl[token.length .. $]; 105 | continue main; 106 | } 107 | } 108 | } 109 | 110 | // could be an identifier or literal 111 | 112 | int pos = 0; 113 | while(pos < decl.length && decl[pos].isIdentifierChar) 114 | pos++; 115 | tokensBuffer ~= decl[0 .. pos]; 116 | decl = decl[pos .. $]; 117 | continue main; 118 | 119 | // FIXME: literals should be handled too 120 | } 121 | 122 | assert(decl.length == 0); // we should have consumed all the input into tokens 123 | 124 | auto tokens = tokensBuffer.slice(); 125 | 126 | 127 | char[64] returnTypeBuffer; 128 | auto returnType = parseAndMangleType(tokens, returnTypeBuffer); 129 | char[256] nameBuffer; 130 | auto name = parseName(tokens, nameBuffer[]); 131 | StackArray!(const(char)[], 64) arguments; 132 | // FIXME: templates and other types of thing should be handled 133 | assert(tokens[0] == "(", "other stuff not implemented " ~ tokens[0]); 134 | tokens = tokens[1 .. $]; 135 | 136 | char[64][32] argTypeBuffers; 137 | int i = 0; 138 | 139 | while(tokens[0] != ")") { 140 | arguments ~= parseAndMangleType(tokens, argTypeBuffers[i]); 141 | i++; 142 | if(tokens[0] == ",") 143 | tokens = tokens[1 .. $]; 144 | } 145 | 146 | assert(tokens[0] == ")", "other stuff not implemented"); 147 | 148 | return mangleFunction(name, returnType, arguments.slice(), buffer); 149 | } 150 | 151 | char[] parseName(ref const(char)[][] tokens, char[] nameBuffer) { 152 | size_t where = 0; 153 | more: 154 | nameBuffer[where .. where + tokens[0].length] = tokens[0][]; 155 | where += tokens[0].length; 156 | tokens = tokens[1 .. $]; 157 | if(tokens.length && tokens[0] == ".") { 158 | tokens = tokens[1 .. $]; 159 | nameBuffer[where++] = '.'; 160 | goto more; 161 | } 162 | 163 | return nameBuffer[0 .. where]; 164 | } 165 | 166 | char[] intToString(int i, char[] buffer) { 167 | int pos = cast(int) buffer.length - 1; 168 | 169 | if(i == 0) { 170 | buffer[pos] = '0'; 171 | pos--; 172 | } 173 | 174 | while(pos > 0 && i) { 175 | buffer[pos] = (i % 10) + '0'; 176 | pos--; 177 | i /= 10; 178 | } 179 | 180 | return buffer[pos + 1 .. $]; 181 | } 182 | 183 | 184 | 185 | char[] mangleName(in char[] name, char[] buffer) { 186 | import std.algorithm; 187 | import std.conv; 188 | 189 | auto parts = name.splitter("."); 190 | 191 | int bufferPos = 0; 192 | foreach(part; parts) { 193 | char[16] numberBuffer; 194 | auto number = intToString(cast(int) part.length, numberBuffer); 195 | 196 | buffer[bufferPos .. bufferPos + number.length] = number[]; 197 | bufferPos += number.length; 198 | 199 | buffer[bufferPos .. bufferPos + part.length] = part[]; 200 | bufferPos += part.length; 201 | } 202 | 203 | return buffer[0 .. bufferPos]; 204 | } 205 | 206 | char[] mangleFunction(in char[] name, in char[] returnTypeMangled, in char[][] argumentsMangle, char[] buffer) { 207 | int bufferPos = 0; 208 | buffer[bufferPos++] = '_'; 209 | buffer[bufferPos++] = 'D'; 210 | 211 | char[256] nameBuffer; 212 | auto mn = mangleName(name, nameBuffer); 213 | buffer[bufferPos .. bufferPos + mn.length] = mn[]; 214 | bufferPos += mn.length; 215 | 216 | buffer[bufferPos++] = 'F'; 217 | foreach(arg; argumentsMangle) { 218 | buffer[bufferPos .. bufferPos + arg.length] = arg[]; 219 | bufferPos += arg.length; 220 | } 221 | buffer[bufferPos++] = 'Z'; 222 | buffer[bufferPos .. bufferPos + returnTypeMangled.length] = returnTypeMangled[]; 223 | bufferPos += returnTypeMangled.length; 224 | 225 | return buffer[0 .. bufferPos]; 226 | } 227 | 228 | char[] parseAndMangleType(ref const(char)[][] tokens, char[] buffer) { 229 | assert(tokens.length); 230 | 231 | int bufferPos = 0; 232 | 233 | void prepend(char p) { 234 | for(int i = bufferPos; i > 0; i--) { 235 | buffer[i] = buffer[i - 1]; 236 | } 237 | buffer[0] = p; 238 | bufferPos++; 239 | } 240 | 241 | // FIXME: handle all the random D type constructors 242 | if(tokens[0] == "const" || tokens[0] == "immutable") { 243 | if(tokens[0] == "const") 244 | buffer[bufferPos++] = 'x'; 245 | else if(tokens[0] == "immutable") 246 | buffer[bufferPos++] = 'y'; 247 | tokens = tokens[1 .. $]; 248 | assert(tokens[0] == "("); 249 | tokens = tokens[1 .. $]; 250 | auto next = parseAndMangleType(tokens, buffer[bufferPos .. $]); 251 | bufferPos += next.length; 252 | assert(tokens[0] == ")"); 253 | tokens = tokens[1 .. $]; 254 | } else { 255 | char primitive = manglePrimitive(tokens[0]); 256 | if(primitive) { 257 | buffer[bufferPos++] = primitive; 258 | tokens = tokens[1 .. $]; 259 | } else { 260 | // probably a struct or something, parse it as an identifier 261 | // FIXME 262 | char[256] nameBuffer; 263 | auto name = parseName(tokens, nameBuffer[]); 264 | 265 | char[256] mangledNameBuffer; 266 | auto mn = mangleName(name, mangledNameBuffer); 267 | 268 | buffer[bufferPos++] = 'S'; 269 | buffer[bufferPos .. bufferPos + mn.length] = mn[]; 270 | bufferPos += mn.length; 271 | } 272 | } 273 | 274 | while(tokens.length) { 275 | if(tokens[0] == "[") { 276 | tokens = tokens[1 .. $]; 277 | prepend('A'); 278 | assert(tokens[0] == "]", "other array not implemented"); 279 | tokens = tokens[1 .. $]; 280 | } else if(tokens[0] == "*") { 281 | prepend('P'); 282 | tokens = tokens[1 .. $]; 283 | } else break; 284 | } 285 | 286 | return buffer[0 .. bufferPos]; 287 | } 288 | 289 | version(unittest) { 290 | int foo(int, string, int); 291 | string foo2(long, char[], int); 292 | struct S { int a; string b; } 293 | S foo3(S, S, string, long, int, S, int[], char[][]); 294 | long testcomplex(int, const(const(char)[]*)[], long); 295 | } 296 | 297 | unittest { 298 | import core.demangle; 299 | char[512] buffer; 300 | 301 | import std.stdio; 302 | assert(mangle(demangle(foo.mangleof), buffer) == foo.mangleof); 303 | assert(mangle(demangle(foo2.mangleof), buffer) == foo2.mangleof); 304 | assert(mangle(demangle(foo3.mangleof), buffer) == foo3.mangleof); 305 | 306 | assert(mangle(demangle(testcomplex.mangleof), buffer) == testcomplex.mangleof); 307 | // FIXME: these all fail if the functions are defined inside the unittest{} block 308 | // so still something wrong parsing those complex names or something 309 | } 310 | 311 | // _D6test303fooFiAyaZi 312 | // _D6test303fooFiAyaZi 313 | 314 | version(unittest) 315 | void main(string[] args) { 316 | 317 | char[512] buffer; 318 | import std.stdio; 319 | if(args.length > 1) 320 | writeln(mangle(args[1], buffer)); 321 | else 322 | writeln(mangle("int test30.foo(int, immutable(char)[])", buffer)); 323 | //mangle("int test30.foo(int, immutable(char)[])", buffer); 324 | } 325 | -------------------------------------------------------------------------------- /minigui_addons/color_dialog.d: -------------------------------------------------------------------------------- 1 | /++ 2 | Displays a color-picker dialog box. On Windows, uses the standard system dialog you know from Paint. On X, uses a custom one with hsla and rgba support. 3 | 4 | History: 5 | Written April 2017. 6 | 7 | Added to dub on December 9, 2021. 8 | +/ 9 | module arsd.minigui_addons.color_dialog; 10 | 11 | import arsd.minigui; 12 | 13 | static if(UsingWin32Widgets) 14 | pragma(lib, "comdlg32"); 15 | 16 | /++ 17 | 18 | +/ 19 | auto showColorDialog(Window owner, Color current, void delegate(Color choice) onOK, void delegate() onCancel = null) { 20 | static if(UsingWin32Widgets) { 21 | import core.sys.windows.windows; 22 | static COLORREF[16] customColors; 23 | CHOOSECOLOR cc; 24 | cc.lStructSize = cc.sizeof; 25 | cc.hwndOwner = owner ? owner.win.impl.hwnd : null; 26 | cc.lpCustColors = cast(LPDWORD) customColors.ptr; 27 | cc.rgbResult = RGB(current.r, current.g, current.b); 28 | cc.Flags = CC_FULLOPEN | CC_RGBINIT; 29 | if(ChooseColor(&cc)) { 30 | onOK(Color(GetRValue(cc.rgbResult), GetGValue(cc.rgbResult), GetBValue(cc.rgbResult))); 31 | } else { 32 | if(onCancel) 33 | onCancel(); 34 | } 35 | } else static if(UsingCustomWidgets) { 36 | auto cpd = new ColorPickerDialog(current, onOK, owner); 37 | cpd.show(); 38 | return cpd; 39 | } else static assert(0); 40 | } 41 | 42 | /* 43 | Hue / Saturation picker 44 | Lightness Picker 45 | 46 | Text selections 47 | 48 | Graphical representation 49 | 50 | Cancel OK 51 | */ 52 | 53 | static if(UsingCustomWidgets) 54 | class ColorPickerDialog : Dialog { 55 | static arsd.simpledisplay.Sprite hslImage; 56 | 57 | static bool canUseImage; 58 | 59 | void delegate(Color) onOK; 60 | 61 | this(Color current, void delegate(Color) onOK, Window owner) { 62 | super(owner, 360, 460, "Color picker"); 63 | 64 | this.onOK = onOK; 65 | 66 | 67 | /* 68 | statusBar.parts ~= new StatusBar.Part(140); 69 | statusBar.parts ~= new StatusBar.Part(140); 70 | statusBar.parts ~= new StatusBar.Part(140); 71 | statusBar.parts ~= new StatusBar.Part(140); 72 | this.addEventListener("mouseover", (Event ev) { 73 | import std.conv; 74 | this.statusBar.parts[2].content = to!string(ev.target.minHeight) ~ " - " ~ to!string(ev.target.maxHeight); 75 | this.statusBar.parts[3].content = ev.target.toString(); 76 | }); 77 | */ 78 | 79 | 80 | static if(UsingSimpledisplayX11) 81 | // it is brutally slow over the network if we don't 82 | // have xshm, so we've gotta do something else. 83 | canUseImage = Image.impl.xshmAvailable; 84 | else 85 | canUseImage = true; 86 | 87 | if(hslImage is null && canUseImage) { 88 | auto img = new TrueColorImage(360, 255); 89 | double h = 0.0, s = 1.0, l = 0.5; 90 | foreach(y; 0 .. img.height) { 91 | foreach(x; 0 .. img.width) { 92 | img.imageData.colors[y * img.width + x] = Color.fromHsl(h,s,l); 93 | h += 360.0 / img.width; 94 | } 95 | h = 0.0; 96 | s -= 1.0 / img.height; 97 | } 98 | 99 | hslImage = new arsd.simpledisplay.Sprite(this.win, Image.fromMemoryImage(img)); 100 | } 101 | 102 | auto t = this; 103 | 104 | auto wid = new class Widget { 105 | this() { super(t); } 106 | override int minHeight() { return hslImage ? hslImage.height : 4; } 107 | override int maxHeight() { return hslImage ? hslImage.height : 4; } 108 | override int marginBottom() { return 4; } 109 | override void paint(WidgetPainter painter) { 110 | if(hslImage) 111 | hslImage.drawAt(painter, Point(0, 0)); 112 | } 113 | }; 114 | 115 | auto hs = new HorizontalSlider(0, 1000, 50, t); 116 | 117 | auto hr = new HorizontalLayout(t); 118 | 119 | auto vlRgb = new VerticalLayout(180, hr); 120 | auto vlHsl = new VerticalLayout(180, hr); 121 | 122 | h = new LabeledLineEdit("Hue:", TextAlignment.Right, vlHsl); 123 | s = new LabeledLineEdit("Saturation:", TextAlignment.Right, vlHsl); 124 | l = new LabeledLineEdit("Lightness:", TextAlignment.Right, vlHsl); 125 | 126 | css = new LabeledLineEdit("CSS:", TextAlignment.Right, vlHsl); 127 | 128 | r = new LabeledLineEdit("Red:", TextAlignment.Right, vlRgb); 129 | g = new LabeledLineEdit("Green:", TextAlignment.Right, vlRgb); 130 | b = new LabeledLineEdit("Blue:", TextAlignment.Right, vlRgb); 131 | a = new LabeledLineEdit("Alpha:", TextAlignment.Right, vlRgb); 132 | 133 | import std.conv; 134 | import std.format; 135 | 136 | double[3] lastHsl; 137 | 138 | void updateCurrent() { 139 | r.content = to!string(current.r); 140 | g.content = to!string(current.g); 141 | b.content = to!string(current.b); 142 | a.content = to!string(current.a); 143 | 144 | auto hsl = current.toHsl; 145 | if(hsl[2] == 0.0 || hsl[2] == 1.0) { 146 | hsl[0 .. 2] = lastHsl[0 .. 2]; 147 | } 148 | 149 | h.content = format("%0.3f", hsl[0]); 150 | s.content = format("%0.3f", hsl[1]); 151 | l.content = format("%0.3f", hsl[2]); 152 | 153 | hs.setPosition(cast(int) (hsl[2] * 1000)); 154 | 155 | css.content = current.toCssString(); 156 | lastHsl = hsl; 157 | } 158 | 159 | updateCurrent(); 160 | 161 | r.addEventListener("focus", &r.selectAll); 162 | g.addEventListener("focus", &g.selectAll); 163 | b.addEventListener("focus", &b.selectAll); 164 | a.addEventListener("focus", &a.selectAll); 165 | 166 | h.addEventListener("focus", &h.selectAll); 167 | s.addEventListener("focus", &s.selectAll); 168 | l.addEventListener("focus", &l.selectAll); 169 | 170 | css.addEventListener("focus", &css.selectAll); 171 | 172 | void convertFromHsl() { 173 | try { 174 | auto c = Color.fromHsl(h.content.to!double, s.content.to!double, l.content.to!double); 175 | c.a = a.content.to!ubyte; 176 | current = c; 177 | updateCurrent(); 178 | } catch(Exception e) { 179 | } 180 | } 181 | 182 | hs.addEventListener((ChangeEvent!int ce) { 183 | // this should only change l, not hs 184 | auto ch = h.content; 185 | auto cs = s.content; 186 | l.content = to!string(ce.value / 1000.0); 187 | convertFromHsl(); 188 | 189 | h.content = ch; 190 | s.content = cs; 191 | }); 192 | 193 | 194 | h.addEventListener("change", &convertFromHsl); 195 | s.addEventListener("change", &convertFromHsl); 196 | l.addEventListener("change", &convertFromHsl); 197 | 198 | css.addEventListener("change", () { 199 | current = Color.fromString(css.content); 200 | updateCurrent(); 201 | }); 202 | 203 | void helper(MouseEventBase event) { 204 | try { 205 | // this should ONLY actually change hue and saturation 206 | 207 | auto h = cast(double) event.clientX / hslImage.width * 360.0; 208 | auto s = 1.0 - (cast(double) event.clientY / hslImage.height * 1.0); 209 | auto oldl = this.l.content; 210 | auto oldhsp = hs.position; 211 | auto l = this.l.content.to!double; 212 | 213 | current = Color.fromHsl(h, s, l); 214 | // import std.stdio; writeln(current.toHsl, " ", h, " ", s, " ", l); 215 | current.a = a.content.to!ubyte; 216 | 217 | updateCurrent(); 218 | 219 | this.l.content = oldl; 220 | hs.setPosition(oldhsp); 221 | 222 | auto e2 = new Event("change", this); 223 | e2.dispatch(); 224 | } catch(Exception e) { 225 | } 226 | } 227 | 228 | if(hslImage !is null) 229 | wid.addEventListener((MouseDownEvent ev) { helper(ev); }); 230 | 231 | if(hslImage !is null) 232 | wid.addEventListener((MouseMoveEvent event) { 233 | if(event.state & ModifierState.leftButtonDown) 234 | helper(event); 235 | }); 236 | 237 | this.addEventListener((KeyDownEvent event) { 238 | if(event.key == Key.Enter || event.key == Key.PadEnter) 239 | OK(); 240 | if(event.key == Key.Escape) 241 | Cancel(); 242 | }); 243 | 244 | this.addEventListener("change", { 245 | redraw(); 246 | }); 247 | 248 | auto s = this; 249 | auto currentColorWidget = new class Widget { 250 | this() { 251 | super(s); 252 | } 253 | 254 | override void paint(WidgetPainter painter) { 255 | auto c = currentColor(); 256 | 257 | auto c1 = alphaBlend(c, Color(64, 64, 64)); 258 | auto c2 = alphaBlend(c, Color(192, 192, 192)); 259 | 260 | painter.outlineColor = c1; 261 | painter.fillColor = c1; 262 | painter.drawRectangle(Point(0, 0), this.width / 2, this.height / 2); 263 | painter.drawRectangle(Point(this.width / 2, this.height / 2), this.width / 2, this.height / 2); 264 | 265 | painter.outlineColor = c2; 266 | painter.fillColor = c2; 267 | painter.drawRectangle(Point(this.width / 2, 0), this.width / 2, this.height / 2); 268 | painter.drawRectangle(Point(0, this.height / 2), this.width / 2, this.height / 2); 269 | } 270 | }; 271 | 272 | auto hl = new HorizontalLayout(this); 273 | auto cancelButton = new Button("Cancel", hl); 274 | auto okButton = new Button("OK", hl); 275 | 276 | recomputeChildLayout(); // FIXME hack 277 | 278 | cancelButton.addEventListener(EventType.triggered, &Cancel); 279 | okButton.addEventListener(EventType.triggered, &OK); 280 | 281 | r.focus(); 282 | } 283 | 284 | LabeledLineEdit r; 285 | LabeledLineEdit g; 286 | LabeledLineEdit b; 287 | LabeledLineEdit a; 288 | 289 | LabeledLineEdit h; 290 | LabeledLineEdit s; 291 | LabeledLineEdit l; 292 | 293 | LabeledLineEdit css; 294 | 295 | Color currentColor() { 296 | import std.conv; 297 | try { 298 | return Color(to!int(r.content), to!int(g.content), to!int(b.content), to!int(a.content)); 299 | } catch(Exception e) { 300 | return Color.transparent; 301 | } 302 | } 303 | 304 | 305 | override void OK() { 306 | import std.conv; 307 | try { 308 | onOK(Color(to!int(r.content), to!int(g.content), to!int(b.content), to!int(a.content))); 309 | this.close(); 310 | } catch(Exception e) { 311 | auto mb = new MessageBox("Bad value"); 312 | mb.show(); 313 | } 314 | } 315 | } 316 | 317 | 318 | -------------------------------------------------------------------------------- /minigui_addons/datetime_picker.d: -------------------------------------------------------------------------------- 1 | /++ 2 | Add-on to [arsd.minigui] to provide date and time widgets. 3 | 4 | History: 5 | Added March 22, 2022 (dub v10.7) 6 | 7 | Bugs: 8 | The Linux implementation is currently extremely minimal. The Windows implementation has more actual graphical functionality. 9 | +/ 10 | module arsd.minigui_addons.datetime_picker; 11 | 12 | import arsd.minigui; 13 | 14 | import std.datetime; 15 | 16 | static if(UsingWin32Widgets) { 17 | import core.sys.windows.windows; 18 | import core.sys.windows.commctrl; 19 | } 20 | 21 | /++ 22 | A DatePicker is a single row input for picking a date. It can drop down a calendar to help the user pick the date they want. 23 | 24 | See also: [TimePicker], [CalendarPicker] 25 | +/ 26 | // on Windows these support a min/max range too 27 | class DatePicker : Widget { 28 | /// 29 | this(Widget parent) { 30 | super(parent); 31 | static if(UsingWin32Widgets) { 32 | createWin32Window(this, "SysDateTimePick32"w, null, 0); 33 | } else { 34 | date = new LabeledLineEdit("Date (YYYY-Mon-DD)", TextAlignment.Right, this); 35 | 36 | date.addEventListener((ChangeEvent!string ce) { changed(); }); 37 | 38 | this.tabStop = false; 39 | } 40 | } 41 | 42 | private Date value_; 43 | 44 | /++ 45 | Current value the user selected. Please note this is NOT valid until AFTER a change event is emitted. 46 | +/ 47 | Date value() { 48 | return value_; 49 | } 50 | 51 | /++ 52 | Changes the current value displayed. Will not send a change event. 53 | +/ 54 | void value(Date v) { 55 | static if(UsingWin32Widgets) { 56 | SYSTEMTIME st; 57 | st.wYear = v.year; 58 | st.wMonth = v.month; 59 | st.wDay = v.day; 60 | SendMessage(hwnd, DTM_SETSYSTEMTIME, GDT_VALID, cast(LPARAM) &st); 61 | } else { 62 | date.content = value_.toSimpleString(); 63 | } 64 | } 65 | 66 | static if(UsingCustomWidgets) private { 67 | LabeledLineEdit date; 68 | string lastMsg; 69 | 70 | void changed() { 71 | try { 72 | value_ = Date.fromSimpleString(date.content); 73 | 74 | this.emit!(ChangeEvent!Date)(&value); 75 | } catch(Exception e) { 76 | if(e.msg != lastMsg) { 77 | messageBox(e.msg); 78 | lastMsg = e.msg; 79 | } 80 | } 81 | } 82 | } 83 | 84 | 85 | 86 | static if(UsingWin32Widgets) { 87 | override int minHeight() { return defaultLineHeight + 6; } 88 | override int maxHeight() { return defaultLineHeight + 6; } 89 | 90 | override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 91 | switch(code) { 92 | case DTN_DATETIMECHANGE: 93 | auto lpChange = cast(LPNMDATETIMECHANGE) hdr; 94 | if(true || (lpChange.dwFlags & GDT_VALID)) { // this flag only set if you use SHOWNONE 95 | auto st = lpChange.st; 96 | value_ = Date(st.wYear, st.wMonth, st.wDay); 97 | 98 | this.emit!(ChangeEvent!Date)(&value); 99 | 100 | mustReturn = true; 101 | } 102 | break; 103 | default: 104 | } 105 | return false; 106 | } 107 | } else { 108 | override int minHeight() { return defaultLineHeight + 4; } 109 | override int maxHeight() { return defaultLineHeight + 4; } 110 | } 111 | 112 | override bool encapsulatedChildren() { 113 | return true; 114 | } 115 | 116 | mixin Emits!(ChangeEvent!Date); 117 | } 118 | 119 | /++ 120 | A TimePicker is a single row input for picking a time. It does not work with timezones. 121 | 122 | See also: [DatePicker] 123 | +/ 124 | class TimePicker : Widget { 125 | /// 126 | this(Widget parent) { 127 | super(parent); 128 | static if(UsingWin32Widgets) { 129 | createWin32Window(this, "SysDateTimePick32"w, null, DTS_TIMEFORMAT); 130 | } else { 131 | time = new LabeledLineEdit("Time", TextAlignment.Right, this); 132 | 133 | time.addEventListener((ChangeEvent!string ce) { changed(); }); 134 | 135 | this.tabStop = false; 136 | } 137 | 138 | } 139 | 140 | private TimeOfDay value_; 141 | 142 | static if(UsingCustomWidgets) private { 143 | LabeledLineEdit time; 144 | string lastMsg; 145 | 146 | void changed() { 147 | try { 148 | value_ = TimeOfDay.fromISOExtString(time.content); 149 | 150 | this.emit!(ChangeEvent!TimeOfDay)(&value); 151 | } catch(Exception e) { 152 | if(e.msg != lastMsg) { 153 | messageBox(e.msg); 154 | lastMsg = e.msg; 155 | } 156 | } 157 | } 158 | } 159 | 160 | 161 | /++ 162 | Current value the user selected. Please note this is NOT valid until AFTER a change event is emitted. 163 | +/ 164 | TimeOfDay value() { 165 | return value_; 166 | } 167 | 168 | /++ 169 | Changes the current value displayed. Will not send a change event. 170 | +/ 171 | void value(TimeOfDay v) { 172 | static if(UsingWin32Widgets) { 173 | SYSTEMTIME st; 174 | st.wHour = v.hour; 175 | st.wMinute = v.minute; 176 | st.wSecond = v.second; 177 | SendMessage(hwnd, DTM_SETSYSTEMTIME, GDT_VALID, cast(LPARAM) &st); 178 | } else { 179 | time.content = value_.toISOExtString(); 180 | } 181 | } 182 | 183 | static if(UsingWin32Widgets) { 184 | override int minHeight() { return defaultLineHeight + 6; } 185 | override int maxHeight() { return defaultLineHeight + 6; } 186 | 187 | override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 188 | switch(code) { 189 | case DTN_DATETIMECHANGE: 190 | auto lpChange = cast(LPNMDATETIMECHANGE) hdr; 191 | if(true || (lpChange.dwFlags & GDT_VALID)) { // this flag only set if you use SHOWNONE 192 | auto st = lpChange.st; 193 | value_ = TimeOfDay(st.wHour, st.wMinute, st.wSecond); 194 | 195 | this.emit!(ChangeEvent!TimeOfDay)(&value); 196 | 197 | mustReturn = true; 198 | } 199 | break; 200 | default: 201 | } 202 | return false; 203 | } 204 | 205 | } else { 206 | override int minHeight() { return defaultLineHeight + 4; } 207 | override int maxHeight() { return defaultLineHeight + 4; } 208 | } 209 | 210 | override bool encapsulatedChildren() { 211 | return true; 212 | } 213 | 214 | mixin Emits!(ChangeEvent!TimeOfDay); 215 | } 216 | 217 | /++ 218 | A CalendarPicker is a rectangular input for picking a date or a range of dates on a 219 | calendar viewer. 220 | 221 | The current value is an [Interval] of dates. Please note that the interval is non-inclusive, 222 | that is, the end day is one day $(I after) the final date the user selected. 223 | 224 | If the user only selected one date, start will be the selection and end is the day after. 225 | +/ 226 | /+ 227 | Note the Windows control also supports bolding dates, changing the max selection count, 228 | week numbers, and more. 229 | +/ 230 | class CalendarPicker : Widget { 231 | /// 232 | this(Widget parent) { 233 | super(parent); 234 | static if(UsingWin32Widgets) { 235 | createWin32Window(this, "SysMonthCal32"w, null, MCS_MULTISELECT); 236 | SendMessage(hwnd, MCM_SETMAXSELCOUNT, int.max, 0); 237 | } else { 238 | start = new LabeledLineEdit("Start", this); 239 | end = new LabeledLineEdit("End", this); 240 | 241 | start.addEventListener((ChangeEvent!string ce) { changed(); }); 242 | end.addEventListener((ChangeEvent!string ce) { changed(); }); 243 | 244 | this.tabStop = false; 245 | } 246 | } 247 | 248 | static if(UsingCustomWidgets) private { 249 | LabeledLineEdit start; 250 | LabeledLineEdit end; 251 | string lastMsg; 252 | 253 | void changed() { 254 | try { 255 | value_ = Interval!Date( 256 | Date.fromSimpleString(start.content), 257 | Date.fromSimpleString(end.content) + 1.days 258 | ); 259 | 260 | this.emit!(ChangeEvent!(Interval!Date))(&value); 261 | } catch(Exception e) { 262 | if(e.msg != lastMsg) { 263 | messageBox(e.msg); 264 | lastMsg = e.msg; 265 | } 266 | } 267 | } 268 | } 269 | 270 | private Interval!Date value_; 271 | 272 | /++ 273 | Current value the user selected. Please note this is NOT valid until AFTER a change event is emitted. 274 | +/ 275 | Interval!Date value() { return value_; } 276 | 277 | /++ 278 | Sets a new interval. Remember, the end date of the interval is NOT included. You might want to `end + 1.days` when creating it. 279 | +/ 280 | void value(Interval!Date v) { 281 | value_ = v; 282 | 283 | auto end = v.end - 1.days; 284 | 285 | static if(UsingWin32Widgets) { 286 | SYSTEMTIME[2] arr; 287 | 288 | arr[0].wYear = v.begin.year; 289 | arr[0].wMonth = v.begin.month; 290 | arr[0].wDay = v.begin.day; 291 | 292 | arr[1].wYear = end.year; 293 | arr[1].wMonth = end.month; 294 | arr[1].wDay = end.day; 295 | 296 | SendMessage(hwnd, MCM_SETSELRANGE, 0, cast(LPARAM) arr.ptr); 297 | } else { 298 | this.start.content = v.begin.toString(); 299 | this.end.content = end.toString(); 300 | } 301 | } 302 | 303 | static if(UsingWin32Widgets) { 304 | override int handleWmNotify(NMHDR* hdr, int code, out int mustReturn) { 305 | switch(code) { 306 | case MCN_SELECT: 307 | auto lpChange = cast(LPNMSELCHANGE) hdr; 308 | auto start = lpChange.stSelStart; 309 | auto end = lpChange.stSelEnd; 310 | 311 | auto et = Date(end.wYear, end.wMonth, end.wDay); 312 | et += dur!"days"(1); 313 | 314 | value_ = Interval!Date( 315 | Date(start.wYear, start.wMonth, start.wDay), 316 | Date(end.wYear, end.wMonth, end.wDay) + 1.days // the interval is non-inclusive 317 | ); 318 | 319 | this.emit!(ChangeEvent!(Interval!Date))(&value); 320 | 321 | mustReturn = true; 322 | break; 323 | default: 324 | } 325 | return false; 326 | } 327 | } 328 | 329 | override bool encapsulatedChildren() { 330 | return true; 331 | } 332 | 333 | mixin Emits!(ChangeEvent!(Interval!Date)); 334 | } 335 | -------------------------------------------------------------------------------- /minigui_addons/keyboard_palette_widget.d: -------------------------------------------------------------------------------- 1 | /++ 2 | The "keyboard palette widget" is a thing for visually representing hotkeys. 3 | 4 | It lays out a bunch of clickable buttons similar to the ascii latter keys on a qwerty keyboard. Each one has an icon and a letter associated with it. If you hold shift, it can change to a second page of items. 5 | 6 | The intention is that input would be layered through some redirects. Normally, ctrl+letters are shortcuts for menu items. Alt+letters are for menu opening. Logo+letters are reserved for the window manager. So this leaves unshifted and shifted keys for you to use. 7 | 8 | My convention is that F-keys are shortcuts for either tabs or menu items. 9 | 10 | Numbers are for switching other things like your brush. 11 | 12 | Letters are thus your main interaction, and symbols can be secondary tool changes. 13 | You could be in one of three modes: 14 | 1) Pick a tool, pick a color, and place with a thing (really mouse interaction mode) 15 | 2) Pick a tool, place colors directly on press (keyboard interaction?) 16 | 3) Pick a color, use a tool 17 | 4) vi style sentences 18 | 19 | It will emit a selection changed event when the user changes the selection. (what about right click? want separate selections for different buttons?) 20 | It will emit a rebind request event when the user tries to rebind it (typically a double click) 21 | 22 | LETTERS (26 total plus shifts) 23 | NUMBERS / NUMPAD (10 total plus shifts) 24 | OTHER KEYS (11 total plus shifts) 25 | ` - = 26 | [ ] \ 27 | ; ' 28 | , . / 29 | 30 | Leaves: tab, space, enter, backspace and of course: arrows, the six pak insert etc. and the F-keys 31 | 32 | Each thing has: Icon / Label / Hotkey / Code or Associated Object. 33 | 34 | USAGE: 35 | 36 | Create a container widget 37 | Put the keyboard palette widget in the container 38 | Put your other content in the container 39 | Make the container get the actual keyboard focus, let this hook into its parent events. 40 | 41 | TOOLS: 42 | place, stamp, select (circle, rectangle, free-form, path) 43 | flood fill 44 | move selection.. move selected contents 45 | scroll (click and drag on the map scrolls the view) 46 | 47 | So really you'd first select tool then select subthing of tool. 48 | 49 | 1 = pen. letters = what thing you're placing 50 | 2 = select. letters = shape or operation of selection 51 | r = rectangle 52 | p = path 53 | c = circle 54 | e = ellipse 55 | q = freeform 56 | a = all 57 | n = none 58 | z = fuzzy (select similar things) 59 | f = flood fill , then letter to pick a color/tile. fill whole selection, fill zone inside selection 60 | m = move 61 | 62 | select wants add, replace, subtract, intersect. 63 | 64 | 2 = replace selection 65 | 3 = add to selection 66 | 4 = remove from selection 67 | 5 = intersect selection 68 | 69 | save/recall selection 70 | save/goto marked scroll place. it can be an icon of a minimap with a highlight: 3x pixel, black/white/black, pointing to it from each edge then a 7x7 px highlight of the thing 71 | +/ 72 | module arsd.minigui_addons.keyboard_palette_widget; 73 | 74 | import arsd.minigui; 75 | 76 | enum KeyGroups { 77 | letters, 78 | numbers, 79 | symbols, 80 | fkeys 81 | } 82 | 83 | struct PaletteItem { 84 | string label; 85 | MemoryImage icon; 86 | Key hotkey; 87 | Object obj; 88 | } 89 | 90 | class KeyboardPaletteWidget : Widget { 91 | this(Widget parent) { 92 | super(parent); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /minigui_addons/nanovega.d: -------------------------------------------------------------------------------- 1 | /++ 2 | An [arsd.minigui] widget that can embed [arsd.nanovega]. 3 | 4 | History: 5 | Added February 7, 2020 (version 9.2) 6 | +/ 7 | module arsd.minigui_addons.nanovega; 8 | 9 | import arsd.minigui; 10 | /// Since the nvg context uses UFCS, you probably want this anyway. 11 | public import arsd.nanovega; 12 | 13 | static if(OpenGlEnabled) 14 | /++ 15 | The NanoVegaWidget has a class you can use with [arsd.nanovega]. 16 | 17 | History: 18 | Included in initial release on February 7, 2020 (dub package version 9.2). 19 | +/ 20 | class NanoVegaWidget : OpenGlWidget { 21 | NVGContext nvg; 22 | 23 | this(Widget parent) { 24 | super(parent); 25 | 26 | win.onClosing = delegate() { 27 | nvg.kill(); 28 | }; 29 | 30 | win.visibleForTheFirstTime = delegate() { 31 | win.setAsCurrentOpenGlContext(); 32 | nvg = nvgCreateContext(); 33 | if(nvg is null) throw new Exception("cannot initialize NanoVega"); 34 | }; 35 | 36 | win.redrawOpenGlScene = delegate() { 37 | if(redrawNVGScene is null) 38 | return; 39 | glViewport(0, 0, this.width, this.height); 40 | if(clearOnEachFrame) { 41 | glClearColor(0, 0, 0, 0); 42 | glClear(glNVGClearFlags); 43 | } 44 | 45 | nvg.beginFrame(this.width, this.height); 46 | scope(exit) nvg.endFrame(); 47 | 48 | redrawNVGScene(nvg); 49 | }; 50 | } 51 | /// Set this to draw your nanovega frame. 52 | void delegate(NVGContext nvg) redrawNVGScene; 53 | 54 | /// If true, it automatically clears the widget canvas between each redraw call. 55 | bool clearOnEachFrame = true; 56 | } 57 | 58 | /// Nanovega requires at least OpenGL 3.0, so this sets that requirement. You can override it later still, of course. 59 | shared static this() { 60 | setOpenGLContextVersion(3, 0); 61 | } 62 | 63 | -------------------------------------------------------------------------------- /minigui_addons/package.d: -------------------------------------------------------------------------------- 1 | /++ 2 | This package consists of additional widgets for [arsd.minigui]. 3 | 4 | Each module stands alone on top of minigui.d; none in this package 5 | depend on each other, so you can pick and choose the modules that 6 | look useful to you and ignore the others. 7 | 8 | These modules may or may not expose native widgets, refer to the 9 | documentation in each individual to see what it does. 10 | 11 | 12 | When writing a minigui addon module, keep the following in mind: 13 | 14 | $(LIST 15 | * Use `static if(UsingWin32Widgets)` and `static if(UsingCustomWidgets)` 16 | if you want to provide both native Windows and custom drawn alternatives. 17 | Do NOT use `version` because versions are not imported across modules. 18 | 19 | * Similarly, if you need to write platform-specific code, you can use 20 | `static if(UsingSimpledisplayX11)` to check for X. However, here, 21 | `version(Windows)` also works pretty well. 22 | 23 | * It is not allowed to import any other minigui_addon module. This is to 24 | ensure it remains individual addons, not a webby mess of a library. 25 | ) 26 | +/ 27 | module arsd.minigui_addons; 28 | -------------------------------------------------------------------------------- /minigui_addons/terminal_emulator_widget.d: -------------------------------------------------------------------------------- 1 | /++ 2 | Creates a UNIX terminal emulator, nested in a minigui widget. 3 | 4 | Depends on my terminalemulator.d core in the arsd repo. 5 | +/ 6 | module arsd.minigui_addons.terminal_emulator_widget; 7 | /// 8 | version(tew_main) 9 | unittest { 10 | import arsd.minigui; 11 | import arsd.minigui_addons.terminal_emulator_widget; 12 | 13 | // version(linux) {} else static assert(0, "Terminal emulation kinda works on other platforms (it runs on Windows, but has no compatible shell program to run there!), but it is actually useful on Linux.") 14 | 15 | void main() { 16 | auto window = new MainWindow("Minigui Terminal Emulation"); 17 | version(Posix) 18 | auto tew = new TerminalEmulatorWidget(["/bin/bash"], window); 19 | else version(Windows) 20 | auto tew = new TerminalEmulatorWidget([`c:\windows\system32\cmd.exe`], window); 21 | window.loop(); 22 | } 23 | 24 | main(); 25 | } 26 | 27 | import arsd.minigui; 28 | 29 | import arsd.terminalemulator; 30 | 31 | class TerminalEmulatorWidget : Widget { 32 | this(Widget parent) { 33 | terminalEmulator = new TerminalEmulatorInsideWidget(this); 34 | super(parent); 35 | } 36 | 37 | mixin Observable!(MemoryImage, "icon"); // please note it can be changed to null! 38 | mixin Observable!(string, "title"); 39 | 40 | this(string[] args, Widget parent) { 41 | version(Windows) { 42 | import core.sys.windows.windows : HANDLE; 43 | void startup(HANDLE inwritePipe, HANDLE outreadPipe) { 44 | terminalEmulator = new TerminalEmulatorInsideWidget(inwritePipe, outreadPipe, this); 45 | } 46 | 47 | import std.string; 48 | startChild!startup(args[0], args.join(" ")); 49 | } 50 | else version(Posix) { 51 | void startup(int master) { 52 | int fd = master; 53 | import fcntl = core.sys.posix.fcntl; 54 | auto flags = fcntl.fcntl(fd, fcntl.F_GETFL, 0); 55 | if(flags == -1) 56 | throw new Exception("fcntl get"); 57 | flags |= fcntl.O_NONBLOCK; 58 | auto s = fcntl.fcntl(fd, fcntl.F_SETFL, flags); 59 | if(s == -1) 60 | throw new Exception("fcntl set"); 61 | 62 | terminalEmulator = new TerminalEmulatorInsideWidget(master, this); 63 | } 64 | 65 | import std.process; 66 | auto cmd = environment.get("SHELL", "/bin/bash"); 67 | startChild!startup(args[0], args); 68 | } 69 | 70 | super(parent); 71 | } 72 | 73 | TerminalEmulatorInsideWidget terminalEmulator; 74 | 75 | override void registerMovement() { 76 | super.registerMovement(); 77 | terminalEmulator.resized(width, height); 78 | } 79 | 80 | override void focus() { 81 | super.focus(); 82 | terminalEmulator.attentionReceived(); 83 | } 84 | 85 | class Style : Widget.Style { 86 | override MouseCursor cursor() { return GenericCursor.Text; } 87 | } 88 | mixin OverrideStyle!Style; 89 | 90 | override void paint(WidgetPainter painter) { 91 | terminalEmulator.redrawPainter(painter, true); 92 | } 93 | } 94 | 95 | 96 | class TerminalEmulatorInsideWidget : TerminalEmulator { 97 | 98 | void resized(int w, int h) { 99 | this.resizeTerminal(w / fontWidth, h / fontHeight); 100 | clearScreenRequested = true; 101 | redraw(); 102 | } 103 | 104 | 105 | protected override void changeCursorStyle(CursorStyle s) { } 106 | 107 | protected override void changeWindowTitle(string t) { 108 | widget.title = t; 109 | } 110 | 111 | // FIXME: minigui TabWidget ought to be able to accept icons too. 112 | protected override void changeWindowIcon(IndexedImage t) { 113 | widget.icon = t; 114 | } 115 | 116 | // FIXME: should we be able to delegate this up the chain too? 117 | protected override void soundBell() { 118 | static if(UsingSimpledisplayX11) 119 | XBell(XDisplayConnection.get(), 50); 120 | } 121 | 122 | protected override void demandAttention() { 123 | // to trigger: echo -e '\033]5001;1\007' 124 | 125 | widget.emitCommand!"requestAttention"; 126 | 127 | // to acknowledge: 128 | // attentionReceived(); 129 | } 130 | 131 | override void requestExit() { 132 | sdpyPrintDebugString("exit"); 133 | widget.emitCommand!"requestExit"; 134 | // FIXME 135 | } 136 | 137 | 138 | protected override void changeIconTitle(string) {} 139 | protected override void changeTextAttributes(TextAttributes) {} 140 | 141 | protected override void copyToClipboard(string text) { 142 | setClipboardText(widget.parentWindow.win, text); 143 | } 144 | 145 | protected override void pasteFromClipboard(void delegate(in char[]) dg) { 146 | static if(UsingSimpledisplayX11) 147 | getPrimarySelection(widget.parentWindow.win, dg); 148 | else 149 | getClipboardText(widget.parentWindow.win, (in char[] dataIn) { 150 | char[] data; 151 | // change Windows \r\n to plain \n 152 | foreach(char ch; dataIn) 153 | if(ch != 13) 154 | data ~= ch; 155 | dg(data); 156 | }); 157 | } 158 | 159 | protected override void copyToPrimary(string text) { 160 | static if(UsingSimpledisplayX11) 161 | setPrimarySelection(widget.parentWindow.win, text); 162 | else 163 | {} 164 | } 165 | protected override void pasteFromPrimary(void delegate(in char[]) dg) { 166 | static if(UsingSimpledisplayX11) 167 | getPrimarySelection(widget.parentWindow.win, dg); 168 | } 169 | 170 | 171 | 172 | void resizeImage() { } 173 | mixin PtySupport!(resizeImage); 174 | 175 | version(Posix) 176 | this(int masterfd, TerminalEmulatorWidget widget) { 177 | master = masterfd; 178 | this(widget); 179 | } 180 | else version(Windows) { 181 | import core.sys.windows.windows; 182 | this(HANDLE stdin, HANDLE stdout, TerminalEmulatorWidget widget) { 183 | this.stdin = stdin; 184 | this.stdout = stdout; 185 | this(widget); 186 | } 187 | } 188 | 189 | bool focused; 190 | 191 | TerminalEmulatorWidget widget; 192 | 193 | mixin SdpyDraw; 194 | 195 | private this(TerminalEmulatorWidget widget) { 196 | 197 | this.widget = widget; 198 | 199 | fontSize = 14; 200 | loadDefaultFont(); 201 | 202 | auto desiredWidth = 80; 203 | auto desiredHeight = 24; 204 | 205 | super(desiredWidth, desiredHeight); 206 | 207 | bool skipNextChar = false; 208 | 209 | widget.addEventListener((MouseDownEvent ev) { 210 | int termX = (ev.clientX - paddingLeft) / fontWidth; 211 | int termY = (ev.clientY - paddingTop) / fontHeight; 212 | 213 | if(sendMouseInputToApplication(termX, termY, 214 | arsd.terminalemulator.MouseEventType.buttonPressed, 215 | cast(arsd.terminalemulator.MouseButton) ev.button, 216 | (ev.state & ModifierState.shift) ? true : false, 217 | (ev.state & ModifierState.ctrl) ? true : false, 218 | (ev.state & ModifierState.alt) ? true : false 219 | )) 220 | redraw(); 221 | }); 222 | 223 | widget.addEventListener((MouseUpEvent ev) { 224 | int termX = (ev.clientX - paddingLeft) / fontWidth; 225 | int termY = (ev.clientY - paddingTop) / fontHeight; 226 | 227 | if(sendMouseInputToApplication(termX, termY, 228 | arsd.terminalemulator.MouseEventType.buttonReleased, 229 | cast(arsd.terminalemulator.MouseButton) ev.button, 230 | (ev.state & ModifierState.shift) ? true : false, 231 | (ev.state & ModifierState.ctrl) ? true : false, 232 | (ev.state & ModifierState.alt) ? true : false 233 | )) 234 | redraw(); 235 | }); 236 | 237 | widget.addEventListener((MouseMoveEvent ev) { 238 | int termX = (ev.clientX - paddingLeft) / fontWidth; 239 | int termY = (ev.clientY - paddingTop) / fontHeight; 240 | 241 | if(sendMouseInputToApplication(termX, termY, 242 | arsd.terminalemulator.MouseEventType.motion, 243 | cast(arsd.terminalemulator.MouseButton) ev.button, 244 | (ev.state & ModifierState.shift) ? true : false, 245 | (ev.state & ModifierState.ctrl) ? true : false, 246 | (ev.state & ModifierState.alt) ? true : false 247 | )) 248 | redraw(); 249 | }); 250 | 251 | widget.addEventListener((KeyDownEvent ev) { 252 | if(ev.key == Key.ScrollLock) { 253 | toggleScrollbackWrap(); 254 | } 255 | 256 | string magic() { 257 | string code; 258 | foreach(member; __traits(allMembers, TerminalKey)) 259 | if(member != "Escape") 260 | code ~= "case Key." ~ member ~ ": if(sendKeyToApplication(TerminalKey." ~ member ~ " 261 | , (ev.state & ModifierState.shift)?true:false 262 | , (ev.state & ModifierState.alt)?true:false 263 | , (ev.state & ModifierState.ctrl)?true:false 264 | , (ev.state & ModifierState.windows)?true:false 265 | )) redraw(); break;"; 266 | return code; 267 | } 268 | 269 | 270 | switch(ev.key) { 271 | //// I want the escape key to send twice to differentiate it from 272 | //// other escape sequences easily. 273 | //case Key.Escape: sendToApplication("\033"); break; 274 | 275 | mixin(magic()); 276 | 277 | default: 278 | // keep going, not special 279 | } 280 | 281 | // remapping of alt+key is possible too, at least on linux. 282 | /+ 283 | static if(UsingSimpledisplayX11) 284 | if(ev.state & ModifierState.alt) { 285 | if(ev.character in altMappings) { 286 | sendToApplication(altMappings[ev.character]); 287 | skipNextChar = true; 288 | } 289 | } 290 | +/ 291 | 292 | return; // the character event handler will do others 293 | }); 294 | 295 | widget.addEventListener((CharEvent ev) { 296 | dchar c = ev.character; 297 | if(skipNextChar) { 298 | skipNextChar = false; 299 | return; 300 | } 301 | 302 | endScrollback(); 303 | char[4] str; 304 | import std.utf; 305 | if(c == '\n') c = '\r'; // terminal seem to expect enter to send 13 instead of 10 306 | auto data = str[0 .. encode(str, c)]; 307 | 308 | // on X11, the delete key can send a 127 character too, but that shouldn't be sent to the terminal since xterm shoots \033[3~ instead, which we handle in the KeyEvent handler. 309 | if(c != 127) 310 | sendToApplication(data); 311 | }); 312 | 313 | version(Posix) { 314 | auto cls = new PosixFdReader(&readyToRead, master); 315 | } else 316 | version(Windows) { 317 | overlapped = new OVERLAPPED(); 318 | overlapped.hEvent = cast(void*) this; 319 | 320 | //window.handleNativeEvent = &windowsRead; 321 | readyToReadWindows(0, 0, overlapped); 322 | redraw(); 323 | } 324 | } 325 | 326 | static int fontSize = 14; 327 | 328 | bool clearScreenRequested = true; 329 | void redraw(bool forceRedraw = false) { 330 | if(widget.parentWindow is null || widget.parentWindow.win is null) 331 | return; 332 | auto painter = widget.draw(); 333 | if(clearScreenRequested) { 334 | auto clearColor = defaultBackground; 335 | painter.outlineColor = clearColor; 336 | painter.fillColor = clearColor; 337 | painter.drawRectangle(Point(0, 0), widget.width, widget.height); 338 | clearScreenRequested = false; 339 | forceRedraw = true; 340 | } 341 | 342 | redrawPainter(painter, forceRedraw); 343 | } 344 | 345 | bool debugMode = false; 346 | } 347 | -------------------------------------------------------------------------------- /mssql.d: -------------------------------------------------------------------------------- 1 | // NOTE: I haven't even tried to use this for a test yet! 2 | // It's probably godawful, if it works at all. 3 | /++ 4 | Implementation of [arsd.database.Database] interface for 5 | Microsoft SQL Server, via ODBC. 6 | +/ 7 | module arsd.mssql; 8 | 9 | version(Windows): 10 | 11 | pragma(lib, "odbc32"); 12 | 13 | public import arsd.database; 14 | 15 | import std.string; 16 | import std.exception; 17 | 18 | import core.sys.windows.sql; 19 | import core.sys.windows.sqlext; 20 | 21 | /// 22 | class MsSql : Database { 23 | /// auto db = new MsSql("Driver={SQL Server Native Client 10.0};Server=[\\];Database=dbtest;Trusted_Connection=Yes") 24 | this(string connectionString) { 25 | SQLAllocHandle(SQL_HANDLE_ENV, cast(void*)SQL_NULL_HANDLE, &env); 26 | enforce(env !is null); 27 | scope(failure) 28 | SQLFreeHandle(SQL_HANDLE_ENV, env); 29 | SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, cast(void *) SQL_OV_ODBC3, 0); 30 | SQLAllocHandle(SQL_HANDLE_DBC, env, &conn); 31 | scope(failure) 32 | SQLFreeHandle(SQL_HANDLE_DBC, conn); 33 | enforce(conn !is null); 34 | 35 | auto ret = SQLDriverConnect( 36 | conn, null, cast(ubyte*)connectionString.ptr, SQL_NTS, 37 | null, 0, null, 38 | SQL_DRIVER_NOPROMPT ); 39 | 40 | if ((ret != SQL_SUCCESS_WITH_INFO) && (ret != SQL_SUCCESS)) 41 | throw new DatabaseException("Unable to connect to ODBC object: " ~ getSQLError(SQL_HANDLE_DBC, conn)); // FIXME: print error 42 | 43 | //query("SET NAMES 'utf8'"); // D does everything with utf8 44 | } 45 | 46 | ~this() { 47 | SQLDisconnect(conn); 48 | SQLFreeHandle(SQL_HANDLE_DBC, conn); 49 | SQLFreeHandle(SQL_HANDLE_ENV, env); 50 | } 51 | 52 | override void startTransaction() { 53 | query("START TRANSACTION"); 54 | } 55 | 56 | // possible fixme, idk if this is right 57 | override string sysTimeToValue(SysTime s) { 58 | return "'" ~ escape(s.toISOExtString()) ~ "'"; 59 | } 60 | 61 | ResultSet queryImpl(string sql, Variant[] args...) { 62 | sql = escapedVariants(this, sql, args); 63 | 64 | // this is passed to MsSqlResult to control 65 | SQLHSTMT statement; 66 | auto returned = SQLAllocHandle(SQL_HANDLE_STMT, conn, &statement); 67 | 68 | enforce(returned == SQL_SUCCESS); 69 | 70 | returned = SQLExecDirect(statement, cast(ubyte*)sql.ptr, cast(SQLINTEGER) sql.length); 71 | if(returned != SQL_SUCCESS) 72 | throw new DatabaseException(getSQLError(SQL_HANDLE_STMT, statement)); 73 | 74 | return new MsSqlResult(statement); 75 | } 76 | 77 | string escape(string sqlData) { // FIXME 78 | return ""; //FIX ME 79 | //return ret.replace("'", "''"); 80 | } 81 | 82 | string escapeBinaryString(const(ubyte)[] data) { // FIXME 83 | return "'" ~ escape(cast(string) data) ~ "'"; 84 | } 85 | 86 | 87 | string error() { 88 | return null; // FIXME 89 | } 90 | 91 | private: 92 | SQLHENV env; 93 | SQLHDBC conn; 94 | } 95 | 96 | class MsSqlResult : ResultSet { 97 | // name for associative array to result index 98 | int getFieldIndex(string field) { 99 | if(mapping is null) 100 | makeFieldMapping(); 101 | if (field !in mapping) 102 | return -1; 103 | return mapping[field]; 104 | } 105 | 106 | 107 | string[] fieldNames() { 108 | if(mapping is null) 109 | makeFieldMapping(); 110 | return columnNames; 111 | } 112 | 113 | // this is a range that can offer other ranges to access it 114 | bool empty() { 115 | return isEmpty; 116 | } 117 | 118 | Row front() { 119 | return row; 120 | } 121 | 122 | void popFront() { 123 | if(!isEmpty) 124 | fetchNext; 125 | } 126 | 127 | override size_t length() 128 | { 129 | return 1; //FIX ME 130 | } 131 | 132 | this(SQLHSTMT statement) { 133 | this.statement = statement; 134 | 135 | SQLSMALLINT info; 136 | SQLNumResultCols(statement, &info); 137 | numFields = info; 138 | 139 | fetchNext(); 140 | } 141 | 142 | ~this() { 143 | SQLFreeHandle(SQL_HANDLE_STMT, statement); 144 | } 145 | 146 | private: 147 | SQLHSTMT statement; 148 | int[string] mapping; 149 | string[] columnNames; 150 | int numFields; 151 | 152 | bool isEmpty; 153 | 154 | Row row; 155 | 156 | void fetchNext() { 157 | if(isEmpty) 158 | return; 159 | 160 | if(SQLFetch(statement) == SQL_SUCCESS) { 161 | Row r; 162 | r.resultSet = this; 163 | DatabaseDatum[] row; 164 | 165 | for(int i = 0; i < numFields; i++) { 166 | string a; 167 | 168 | SQLLEN ptr; 169 | 170 | more: 171 | SQLCHAR[1024] buf; 172 | if(SQLGetData(statement, cast(ushort)(i+1), SQL_CHAR, buf.ptr, 1024, &ptr) != SQL_SUCCESS) 173 | throw new DatabaseException("get data: " ~ getSQLError(SQL_HANDLE_STMT, statement)); 174 | 175 | assert(ptr != SQL_NO_TOTAL); 176 | if(ptr == SQL_NULL_DATA) 177 | a = null; 178 | else { 179 | a ~= cast(string) buf[0 .. ptr > 1024 ? 1024 : ptr].idup; 180 | ptr -= ptr > 1024 ? 1024 : ptr; 181 | if(ptr) 182 | goto more; 183 | } 184 | row ~= DatabaseDatum(a); 185 | } 186 | 187 | r.row = row; 188 | this.row = r; 189 | } else { 190 | isEmpty = true; 191 | } 192 | } 193 | 194 | void makeFieldMapping() { 195 | for(int i = 0; i < numFields; i++) { 196 | SQLSMALLINT len; 197 | SQLCHAR[1024] buf; 198 | auto ret = SQLDescribeCol(statement, 199 | cast(ushort)(i+1), 200 | cast(ubyte*)buf.ptr, 201 | 1024, 202 | &len, 203 | null, null, null, null); 204 | if (ret != SQL_SUCCESS) 205 | throw new DatabaseException("Field mapping error: " ~ getSQLError(SQL_HANDLE_STMT, statement)); 206 | 207 | string a = cast(string) buf[0 .. len].idup; 208 | 209 | columnNames ~= a; 210 | mapping[a] = i; 211 | } 212 | 213 | } 214 | } 215 | 216 | private string getSQLError(short handletype, SQLHANDLE handle) 217 | { 218 | char[32] sqlstate; 219 | char[256] message; 220 | SQLINTEGER nativeerror=0; 221 | SQLSMALLINT textlen=0; 222 | auto ret = SQLGetDiagRec(handletype, handle, 1, 223 | cast(ubyte*)sqlstate.ptr, 224 | cast(int*)&nativeerror, 225 | cast(ubyte*)message.ptr, 226 | 256, 227 | &textlen); 228 | 229 | return message[0 .. textlen].idup; 230 | } 231 | 232 | /* 233 | import std.stdio; 234 | void main() { 235 | //auto db = new MsSql("Driver={SQL Server};Server=[\\]>;Database=dbtest;Trusted_Connection=Yes"); 236 | auto db = new MsSql("Driver={SQL Server Native Client 10.0};Server=[\\];Database=dbtest;Trusted_Connection=Yes") 237 | 238 | db.query("INSERT INTO users (id, name) values (30, 'hello mang')"); 239 | 240 | foreach(line; db.query("SELECT * FROM users")) { 241 | writeln(line[0], line["name"]); 242 | } 243 | } 244 | */ 245 | 246 | void omg() { 247 | 248 | enum EMPLOYEE_ID_LEN = 6 ; 249 | 250 | SQLHENV henv = null; 251 | SQLHDBC hdbc = null; 252 | SQLRETURN retcode; 253 | SQLHSTMT hstmt = null; 254 | SQLSMALLINT sCustID; 255 | 256 | SQLCHAR[EMPLOYEE_ID_LEN]szEmployeeID; 257 | SQL_DATE_STRUCT dsOrderDate; 258 | SQLINTEGER cbCustID = 0, cbOrderDate = 0, cbEmployeeID = SQL_NTS; 259 | 260 | retcode = SQLAllocHandle(SQL_HANDLE_ENV, cast(void*) SQL_NULL_HANDLE, &henv); 261 | retcode = SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, cast(SQLPOINTER*)SQL_OV_ODBC3, 0); 262 | 263 | retcode = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc); 264 | retcode = SQLSetConnectAttr(hdbc, SQL_LOGIN_TIMEOUT, cast(SQLPOINTER)5, 0); 265 | 266 | retcode = SQLDriverConnect( 267 | hdbc, null, cast(ubyte*)"DSN=PostgreSQL30Postgres".ptr, SQL_NTS, 268 | null, 0, null, 269 | SQL_DRIVER_NOPROMPT ); 270 | 271 | 272 | import std.stdio; writeln(retcode); 273 | retcode = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt); 274 | 275 | szEmployeeID[0 .. 6] = cast(ubyte[]) "BERGS\0"; 276 | 277 | sCustID = 5; 278 | dsOrderDate.year = 2006; 279 | dsOrderDate.month = 3; 280 | dsOrderDate.day = 17; 281 | 282 | 283 | /* 284 | retcode = SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, EMPLOYEE_ID_LEN, 0, szEmployeeID.ptr, 0, &cbEmployeeID); 285 | import std.stdio; writeln(retcode); writeln(getSQLError(SQL_HANDLE_STMT, hstmt)); 286 | */ 287 | retcode = SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_SSHORT, SQL_INTEGER, 0, 0, &sCustID, 0, &cbCustID); 288 | import std.stdio; writeln(retcode); writeln(getSQLError(SQL_HANDLE_STMT, hstmt)); 289 | /* 290 | retcode = SQLBindParameter(hstmt, 3, SQL_PARAM_INPUT, SQL_C_TYPE_DATE, SQL_TIMESTAMP, dsOrderDate.sizeof, 0, &dsOrderDate, 0, &cbOrderDate); 291 | import std.stdio; writeln(retcode); writeln(getSQLError(SQL_HANDLE_STMT, hstmt)); 292 | */ 293 | 294 | retcode = SQLPrepare(hstmt, cast(SQLCHAR*)"INSERT INTO Orders(CustomerID, EmployeeID, OrderDate) VALUES ('omg', ?, 'now')", SQL_NTS); 295 | 296 | import std.stdio; writeln("here ", retcode); writeln(getSQLError(SQL_HANDLE_STMT, hstmt)); 297 | 298 | retcode = SQLExecute(hstmt); 299 | import std.stdio; writeln(retcode); writeln(getSQLError(SQL_HANDLE_STMT, hstmt)); 300 | } 301 | -------------------------------------------------------------------------------- /mvd.d: -------------------------------------------------------------------------------- 1 | /++ 2 | mvd stands for Multiple Virtual Dispatch. It lets you 3 | write functions that take any number of arguments of 4 | objects and match based on the dynamic type of each 5 | of them. 6 | 7 | --- 8 | void foo(Object a, Object b) {} // 1 9 | void foo(MyClass b, Object b) {} // 2 10 | void foo(DerivedClass a, MyClass b) {} // 3 11 | 12 | Object a = new MyClass(); 13 | Object b = new Object(); 14 | 15 | mvd!foo(a, b); // will call overload #2 16 | --- 17 | 18 | The return values must be compatible; [mvd] will return 19 | the least specialized static type of the return values 20 | (most likely the shared base class type of all return types, 21 | or `void` if there isn't one). 22 | 23 | All non-class/interface types should be compatible among overloads. 24 | Otherwise you are liable to get compile errors. (Or it might work, 25 | that's up to the compiler's discretion.) 26 | +/ 27 | module arsd.mvd; 28 | 29 | import std.traits; 30 | 31 | /// This exists just to make the documentation of [mvd] nicer looking. 32 | template CommonReturnOfOverloads(alias fn) { 33 | alias overloads = __traits(getOverloads, __traits(parent, fn), __traits(identifier, fn)); 34 | static if (overloads.length == 1) { 35 | alias CommonReturnOfOverloads = ReturnType!(overloads[0]); 36 | } 37 | else { 38 | alias CommonReturnOfOverloads = CommonType!(staticMap!(ReturnType, overloads)); 39 | } 40 | } 41 | 42 | /// See details on the [arsd.mvd] page. 43 | CommonReturnOfOverloads!fn mvd(alias fn, T...)(T args) { 44 | return mvdObj!fn(null, args); 45 | } 46 | 47 | CommonReturnOfOverloads!fn mvdObj(alias fn, This, T...)(This this_, T args) { 48 | typeof(return) delegate() bestMatch; 49 | int bestScore; 50 | 51 | string argsStr() { 52 | string s; 53 | foreach(arg; args) { 54 | if(s.length) 55 | s ~= ", "; 56 | static if (is(typeof(arg) == class)) { 57 | if (arg is null) { 58 | s ~= "null " ~ typeof(arg).stringof; 59 | } else { 60 | s ~= typeid(arg).name; 61 | } 62 | } else { 63 | s ~= typeof(arg).stringof; 64 | } 65 | } 66 | return s; 67 | } 68 | 69 | ov: foreach(overload; __traits(getOverloads, __traits(parent, fn), __traits(identifier, fn))) { 70 | Parameters!overload pargs; 71 | int score = 0; 72 | foreach(idx, parg; pargs) { 73 | alias t = typeof(parg); 74 | static if(is(t == interface) || is(t == class)) { 75 | t value = cast(t) args[idx]; 76 | // HACK: cast to Object* so we can set the value even if it's an immutable class 77 | *cast(Object*) &pargs[idx] = cast(Object) value; 78 | if(args[idx] !is null && pargs[idx] is null) 79 | continue ov; // failed cast, forget it 80 | else 81 | score += BaseClassesTuple!t.length + 1; 82 | } else 83 | pargs[idx] = args[idx]; 84 | } 85 | if(score == bestScore) 86 | throw new Exception("ambiguous overload selection with args (" ~ argsStr ~ ")"); 87 | if(score > bestScore) { 88 | bestMatch = () { 89 | static if(is(typeof(return) == void)) 90 | __traits(child, this_, overload)(pargs); 91 | else 92 | return __traits(child, this_, overload)(pargs); 93 | }; 94 | bestScore = score; 95 | } 96 | } 97 | 98 | if(bestMatch is null) 99 | throw new Exception("no match existed with args (" ~ argsStr ~ ")"); 100 | 101 | return bestMatch(); 102 | } 103 | 104 | /// 105 | unittest { 106 | 107 | class MyClass {} 108 | class DerivedClass : MyClass {} 109 | class OtherClass {} 110 | 111 | static struct Wrapper { 112 | static: // this is just a namespace cuz D doesn't allow overloading inside unittest 113 | int foo(Object a, Object b) { return 1; } 114 | int foo(MyClass a, Object b) { return 2; } 115 | int foo(DerivedClass a, MyClass b) { return 3; } 116 | 117 | int bar(MyClass a) { return 4; } 118 | } 119 | 120 | with(Wrapper) { 121 | assert(mvd!foo(new Object, new Object) == 1); 122 | assert(mvd!foo(new MyClass, new DerivedClass) == 2); 123 | assert(mvd!foo(new DerivedClass, new DerivedClass) == 3); 124 | assert(mvd!foo(new OtherClass, new OtherClass) == 1); 125 | assert(mvd!foo(new OtherClass, new MyClass) == 1); 126 | assert(mvd!foo(new DerivedClass, new DerivedClass) == 3); 127 | assert(mvd!foo(new OtherClass, new MyClass) == 1); 128 | 129 | //mvd!bar(new OtherClass); 130 | } 131 | } 132 | 133 | /// 134 | unittest { 135 | 136 | class MyClass {} 137 | class DerivedClass : MyClass {} 138 | class OtherClass {} 139 | 140 | class Wrapper { 141 | int x; 142 | 143 | int foo(Object a, Object b) { return x + 1; } 144 | int foo(MyClass a, Object b) { return x + 2; } 145 | int foo(DerivedClass a, MyClass b) { return x + 3; } 146 | 147 | int bar(MyClass a) { return x + 4; } 148 | } 149 | 150 | Wrapper wrapper = new Wrapper; 151 | wrapper.x = 20; 152 | assert(wrapper.mvdObj!(wrapper.foo)(new Object, new Object) == 21); 153 | assert(wrapper.mvdObj!(wrapper.foo)(new MyClass, new DerivedClass) == 22); 154 | assert(wrapper.mvdObj!(wrapper.foo)(new DerivedClass, new DerivedClass) == 23); 155 | assert(wrapper.mvdObj!(wrapper.foo)(new OtherClass, new OtherClass) == 21); 156 | assert(wrapper.mvdObj!(wrapper.foo)(new OtherClass, new MyClass) == 21); 157 | assert(wrapper.mvdObj!(wrapper.foo)(new DerivedClass, new DerivedClass) == 23); 158 | assert(wrapper.mvdObj!(wrapper.foo)(new OtherClass, new MyClass) == 21); 159 | 160 | //mvd!bar(new OtherClass); 161 | } 162 | 163 | /// 164 | unittest { 165 | class MyClass {} 166 | 167 | static bool success = false; 168 | 169 | static struct Wrapper { 170 | static: 171 | void foo(MyClass a) { success = true; } 172 | } 173 | 174 | with(Wrapper) { 175 | mvd!foo(new MyClass); 176 | assert(success); 177 | } 178 | } 179 | 180 | /// 181 | unittest { 182 | immutable class Foo {} 183 | 184 | immutable class Bar : Foo { 185 | int x; 186 | 187 | this(int x) { 188 | this.x = x; 189 | } 190 | } 191 | 192 | immutable class Baz : Foo { 193 | int x, y; 194 | 195 | this(int x, int y) { 196 | this.x = x; 197 | this.y = y; 198 | } 199 | } 200 | 201 | static struct Wrapper { 202 | static: 203 | 204 | int foo(Bar b) { return b.x; } 205 | int foo(Baz b) { return b.x + b.y; } 206 | } 207 | 208 | with(Wrapper) { 209 | Foo x = new Bar(3); 210 | Foo y = new Baz(5, 7); 211 | assert(mvd!foo(x) == 3); 212 | assert(mvd!foo(y) == 12); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /package.d: -------------------------------------------------------------------------------- 1 | /++ 2 | This package contains a variety of independent modules that I have 3 | written over my years of using D. 4 | 5 | You can usually use them independently, with few or no dependencies, 6 | so it is easy to use raw, or you can use dub packages as well. 7 | 8 | See [arsd.docs] for top-level documents in addition to what is below. 9 | 10 | What are you working with? (minimal starting points now but im working on it) 11 | 12 | ${RAW_HTML 13 | 43 | } 44 | 45 | $(LIST 46 | $(CLASS category-grid) 47 | 48 | * [#web|Web] 49 | * [#desktop|Desktop] 50 | * [#terminals|Terminals] 51 | * [#databases|Databases] 52 | * [#scripting|Scripting] 53 | * [#email|Email] 54 | ) 55 | 56 | 57 | $(H2 Categories) 58 | 59 | $(H3 Web) 60 | $(LIST 61 | $(CLASS category-grid) 62 | 63 | * [#web-server|Server-side code] 64 | * [#web-api-client|Consuming HTTP APIs] 65 | * [#web-scraper|Scraping Web Pages] 66 | ) 67 | 68 | $(H4 $(ID web-server) Server-side code) 69 | See [arsd.cgi] 70 | 71 | $(H4 $(ID web-api-client) Consuming HTTP APIs) 72 | See [arsd.http2] 73 | 74 | $(H4 $(ID web-scraper) Scraping Web Pages) 75 | See [arsd.dom.Document.fromUrl] 76 | 77 | $(H3 Desktop) 78 | $(LIST 79 | $(CLASS category-grid) 80 | 81 | * [#desktop-game|Game] 82 | * [#desktop-gui|GUIs] 83 | * [#desktop-webview|WebView] 84 | ) 85 | 86 | $(H4 $(ID desktop-game) Games) 87 | See [arsd.simpledisplay] and [arsd.gamehelpers]. 88 | 89 | Check out [arsd.pixmappresenter] for old-skool games that blit fully-rendered frames to the screen. 90 | 91 | $(H4 $(ID desktop-gui) GUIs) 92 | See [arsd.minigui], [arsd.nanovega], and also: https://github.com/drug007/nanogui 93 | 94 | You can also do it yourself with [arsd.simpledisplay]. 95 | 96 | $(H4 $(ID desktop-webview) WebView) 97 | This is a work in progress, but see [arsd.webview] 98 | $(H3 Terminals) 99 | $(LIST 100 | $(CLASS category-grid) 101 | 102 | * [#terminal-line|Line-based] 103 | * [#terminal-full|Full screen] 104 | * [#terminal-html|HTML dump] 105 | ) 106 | 107 | $(H4 $(ID terminal-line) Line-based) 108 | See [arsd.terminal] 109 | 110 | $(H4 $(ID terminal-full) Full screen) 111 | See [arsd.terminal] 112 | 113 | $(H4 $(ID terminal-html) HTML dump) 114 | See [arsd.terminal] and [arsd.htmltotext] 115 | 116 | $(H3 Databases) 117 | $(LIST 118 | $(CLASS category-grid) 119 | 120 | * [#database-sql|SQL queries] 121 | * [#database-orm|Minimal ORM] 122 | ) 123 | 124 | $(H4 $(ID database-sql) SQL queries) 125 | See [arsd.database], [arsd.mysql], [arsd.postgres], [arsd.sqlite], and [arsd.mssql]. 126 | 127 | $(H4 $(ID database-orm) Minimal ORM) 128 | See [arsd.database_generation] as well as parts in [arsd.database]. 129 | 130 | $(H3 Scripting) 131 | See [arsd.script] 132 | 133 | $(H3 Email) 134 | $(LIST 135 | $(CLASS category-grid) 136 | 137 | * [#email-sending|Sending Plain Email] 138 | * [#email-mime|Sending HTML Email] 139 | * [#email-processing|Processing Email] 140 | ) 141 | 142 | $(H4 $(ID email-sending) Sending Plain Email) 143 | See [arsd.email] 144 | $(H4 $(ID email-mime) Sending HTML Email) 145 | See [arsd.email] 146 | $(H4 $(ID email-processing) Processing Email) 147 | See [arsd.email] 148 | +/ 149 | module arsd; 150 | -------------------------------------------------------------------------------- /postgres.d: -------------------------------------------------------------------------------- 1 | /++ 2 | Uses libpq implement the [arsd.database.Database] interface. 3 | 4 | Requires the official pq library from Postgres to be installed to build 5 | and to use. Note that on Windows, it is often distributed as `libpq.lib`. 6 | You will have to copy or rename that to `pq.lib` for dub or dmd to automatically 7 | find it. You will also likely need to add the lib search path yourself on 8 | both Windows and Linux systems (on my Linux box, it is `-L-L/usr/local/pgsql/lib` 9 | to dmd. You can also list things your app's dub.json's lflags too. Note on the 10 | Microsoft linker, the flag is called `/LIBPATH`.) 11 | 12 | For example, for the default Postgres install on Windows, try: 13 | 14 | ``` 15 | "lflags-windows": [ "/LIBPATH:C:/Program Files/PostgreSQL//lib" ], 16 | ``` 17 | 18 | In your dub.json. 19 | 20 | When you distribute your application, the user will want to install libpq client on 21 | Linux, and on Windows, you may want to include the libpq.dll in your distribution. 22 | Note it may also depend on OpenSSL ssl and crypto dlls and libintl.dll as well. These 23 | should be found in the PostgreSQL lib and/or bin folders (check them both!). 24 | +/ 25 | module arsd.postgres; 26 | 27 | version(Windows) 28 | pragma(lib, "libpq"); 29 | else 30 | pragma(lib, "pq"); 31 | 32 | public import arsd.database; 33 | 34 | import std.string; 35 | import std.exception; 36 | 37 | // remember to CREATE DATABASE name WITH ENCODING 'utf8' 38 | // 39 | // http://www.postgresql.org/docs/8.0/static/libpq-exec.html 40 | // ExecParams, PQPrepare, PQExecPrepared 41 | // 42 | // SQL: `DEALLOCATE name` is how to dealloc a prepared statement. 43 | 44 | /++ 45 | The PostgreSql implementation of the [Database] interface. 46 | 47 | You should construct this class, but then use it through the 48 | interface functions. 49 | 50 | --- 51 | auto db = new PostgreSql("dbname=name"); 52 | foreach(row; db.query("SELECT id, data FROM table_name")) 53 | writeln(row[0], " = ", row[1]); 54 | --- 55 | +/ 56 | class PostgreSql : Database { 57 | /// `dbname=your_database_name` is probably the most common connection string. See section "33.1.1.1. Keyword/Value Connection Strings" on https://www.postgresql.org/docs/10/libpq-connect.html 58 | this(string connectionString) { 59 | this.connectionString = connectionString; 60 | conn = PQconnectdb(toStringz(connectionString)); 61 | if(conn is null) 62 | throw new DatabaseException("Unable to allocate PG connection object"); 63 | if(PQstatus(conn) != CONNECTION_OK) 64 | throw new DatabaseException(error()); 65 | query("SET NAMES 'utf8'"); // D does everything with utf8 66 | } 67 | 68 | string connectionString; 69 | 70 | ~this() { 71 | PQfinish(conn); 72 | } 73 | 74 | string sysTimeToValue(SysTime s) { 75 | return "'" ~ escape(s.toISOExtString()) ~ "'::timestamptz"; 76 | } 77 | 78 | /** 79 | Prepared statement support 80 | 81 | This will be added to the Database interface eventually in some form, 82 | but first I need to implement it for all my providers. 83 | 84 | The common function of those 4 will be what I put in the interface. 85 | */ 86 | 87 | ResultSet executePreparedStatement(T...)(string name, T args) { 88 | const(char)*[args.length] argsStrings; 89 | 90 | foreach(idx, arg; args) { 91 | // FIXME: optimize to remove allocations here 92 | import std.conv; 93 | static if(!is(typeof(arg) == typeof(null))) 94 | argsStrings[idx] = toStringz(to!string(arg)); 95 | // else make it null 96 | } 97 | 98 | auto res = PQexecPrepared(conn, toStringz(name), argsStrings.length, argsStrings.ptr, null, null, 0); 99 | 100 | int ress = PQresultStatus(res); 101 | if(ress != PGRES_TUPLES_OK 102 | && ress != PGRES_COMMAND_OK) 103 | throw new DatabaseException(error()); 104 | 105 | return new PostgresResult(res); 106 | 107 | } 108 | 109 | /// 110 | override void startTransaction() { 111 | query("START TRANSACTION"); 112 | } 113 | 114 | ResultSet queryImpl(string sql, Variant[] args...) { 115 | sql = escapedVariants(this, sql, args); 116 | 117 | bool first_retry = true; 118 | 119 | retry: 120 | 121 | auto res = PQexec(conn, toStringz(sql)); 122 | int ress = PQresultStatus(res); 123 | // https://www.postgresql.org/docs/current/libpq-exec.html 124 | // FIXME: PQresultErrorField can get a lot more info in a more structured way 125 | if(ress != PGRES_TUPLES_OK 126 | && ress != PGRES_COMMAND_OK) 127 | { 128 | if(first_retry && error() == "no connection to the server\n") { 129 | first_retry = false; 130 | // try to reconnect... 131 | PQfinish(conn); 132 | conn = PQconnectdb(toStringz(connectionString)); 133 | if(conn is null) 134 | throw new DatabaseException("Unable to allocate PG connection object"); 135 | if(PQstatus(conn) != CONNECTION_OK) 136 | throw new DatabaseException(error()); 137 | goto retry; 138 | } 139 | throw new DatabaseException(error()); 140 | } 141 | 142 | return new PostgresResult(res); 143 | } 144 | 145 | string escape(string sqlData) { 146 | char* buffer = (new char[sqlData.length * 2 + 1]).ptr; 147 | ulong size = PQescapeString (buffer, sqlData.ptr, sqlData.length); 148 | 149 | string ret = assumeUnique(buffer[0.. cast(size_t) size]); 150 | 151 | return ret; 152 | } 153 | 154 | string escapeBinaryString(const(ubyte)[] data) { 155 | // must include '\x ... ' here 156 | size_t len; 157 | char* buf = PQescapeByteaConn(conn, data.ptr, data.length, &len); 158 | if(buf is null) 159 | throw new Exception("pgsql out of memory escaping binary string"); 160 | 161 | string res; 162 | if(len == 0) 163 | res = "''"; 164 | else 165 | res = cast(string) ("'" ~ buf[0 .. len - 1] ~ "'"); // gotta cut the zero terminator off 166 | 167 | PQfreemem(buf); 168 | 169 | return res; 170 | } 171 | 172 | 173 | /// 174 | string error() { 175 | return copyCString(PQerrorMessage(conn)); 176 | } 177 | 178 | private: 179 | PGconn* conn; 180 | } 181 | 182 | private string toLowerFast(string s) { 183 | import std.ascii : isUpper; 184 | foreach (c; s) 185 | if (c >= 0x80 || isUpper(c)) 186 | return toLower(s); 187 | return s; 188 | } 189 | 190 | /// 191 | class PostgresResult : ResultSet { 192 | // name for associative array to result index 193 | int getFieldIndex(string field) { 194 | if(mapping is null) 195 | makeFieldMapping(); 196 | field = field.toLowerFast; 197 | if(field in mapping) 198 | return mapping[field]; 199 | else throw new Exception("no mapping " ~ field); 200 | } 201 | 202 | 203 | string[] fieldNames() { 204 | if(mapping is null) 205 | makeFieldMapping(); 206 | return columnNames; 207 | } 208 | 209 | // this is a range that can offer other ranges to access it 210 | bool empty() { 211 | return position == numRows; 212 | } 213 | 214 | Row front() { 215 | return row; 216 | } 217 | 218 | int affectedRows() @system { 219 | auto g = PQcmdTuples(res); 220 | if(g is null) 221 | return 0; 222 | int num; 223 | while(*g) { 224 | num *= 10; 225 | num += *g - '0'; 226 | g++; 227 | } 228 | return num; 229 | } 230 | 231 | void popFront() { 232 | position++; 233 | if(position < numRows) 234 | fetchNext(); 235 | } 236 | 237 | override size_t length() { 238 | return numRows; 239 | } 240 | 241 | this(PGresult* res) { 242 | this.res = res; 243 | numFields = PQnfields(res); 244 | numRows = PQntuples(res); 245 | 246 | if(numRows) 247 | fetchNext(); 248 | } 249 | 250 | ~this() { 251 | PQclear(res); 252 | } 253 | 254 | private: 255 | PGresult* res; 256 | int[string] mapping; 257 | string[] columnNames; 258 | int numFields; 259 | 260 | int position; 261 | 262 | int numRows; 263 | 264 | Row row; 265 | 266 | void fetchNext() { 267 | Row r; 268 | r.resultSet = this; 269 | DatabaseDatum[] row; 270 | 271 | for(int i = 0; i < numFields; i++) { 272 | string a; 273 | 274 | if(PQgetisnull(res, position, i)) 275 | a = null; 276 | else { 277 | switch(PQfformat(res, i)) { 278 | case 0: // text representation 279 | switch(PQftype(res, i)) { 280 | case BYTEAOID: 281 | size_t len; 282 | char* c = PQunescapeBytea(PQgetvalue(res, position, i), &len); 283 | 284 | a = cast(string) c[0 .. len].idup; 285 | 286 | PQfreemem(c); 287 | break; 288 | default: 289 | a = copyCString(PQgetvalue(res, position, i), PQgetlength(res, position, i)); 290 | } 291 | break; 292 | case 1: // binary representation 293 | throw new Exception("unexpected format returned by pq"); 294 | default: 295 | throw new Exception("unknown pq format"); 296 | } 297 | 298 | } 299 | row ~= DatabaseDatum(a); 300 | } 301 | 302 | r.row = row; 303 | this.row = r; 304 | } 305 | 306 | void makeFieldMapping() { 307 | for(int i = 0; i < numFields; i++) { 308 | string a = copyCString(PQfname(res, i)); 309 | 310 | columnNames ~= a; 311 | mapping[a] = i; 312 | } 313 | 314 | } 315 | } 316 | 317 | string copyCString(const char* c, int actualLength = -1) @system { 318 | const(char)* a = c; 319 | if(a is null) 320 | return null; 321 | 322 | string ret; 323 | if(actualLength == -1) 324 | while(*a) { 325 | ret ~= *a; 326 | a++; 327 | } 328 | else { 329 | ret = a[0..actualLength].idup; 330 | } 331 | 332 | return ret; 333 | } 334 | 335 | extern(C) { 336 | struct PGconn {}; 337 | struct PGresult {}; 338 | 339 | void PQfinish(PGconn*); 340 | PGconn* PQconnectdb(const char*); 341 | 342 | int PQstatus(PGconn*); // FIXME check return value 343 | 344 | const (char*) PQerrorMessage(PGconn*); 345 | 346 | PGresult* PQexec(PGconn*, const char*); 347 | void PQclear(PGresult*); 348 | 349 | PGresult* PQprepare(PGconn*, const char* stmtName, const char* query, int nParams, const void* paramTypes); 350 | 351 | PGresult* PQexecPrepared(PGconn*, const char* stmtName, int nParams, const char** paramValues, const int* paramLengths, const int* paramFormats, int resultFormat); 352 | 353 | int PQresultStatus(PGresult*); // FIXME check return value 354 | 355 | int PQnfields(PGresult*); // number of fields in a result 356 | const(char*) PQfname(PGresult*, int); // name of field 357 | 358 | int PQntuples(PGresult*); // number of rows in result 359 | const(char*) PQgetvalue(PGresult*, int row, int column); 360 | 361 | size_t PQescapeString (char *to, const char *from, size_t length); 362 | 363 | enum int CONNECTION_OK = 0; 364 | enum int PGRES_COMMAND_OK = 1; 365 | enum int PGRES_TUPLES_OK = 2; 366 | 367 | int PQgetlength(const PGresult *res, 368 | int row_number, 369 | int column_number); 370 | int PQgetisnull(const PGresult *res, 371 | int row_number, 372 | int column_number); 373 | 374 | int PQfformat(const PGresult *res, int column_number); 375 | 376 | alias Oid = int; 377 | enum BYTEAOID = 17; 378 | Oid PQftype(const PGresult* res, int column_number); 379 | 380 | char *PQescapeByteaConn(PGconn *conn, 381 | const ubyte *from, 382 | size_t from_length, 383 | size_t *to_length); 384 | char *PQunescapeBytea(const char *from, size_t *to_length); 385 | void PQfreemem(void *ptr); 386 | 387 | char* PQcmdTuples(PGresult *res); 388 | 389 | } 390 | 391 | /* 392 | import std.stdio; 393 | void main() { 394 | auto db = new PostgreSql("dbname = test"); 395 | 396 | db.query("INSERT INTO users (id, name) values (?, ?)", 30, "hello mang"); 397 | 398 | foreach(line; db.query("SELECT * FROM users")) { 399 | writeln(line[0], line["name"]); 400 | } 401 | } 402 | */ 403 | -------------------------------------------------------------------------------- /pptx.d: -------------------------------------------------------------------------------- 1 | /++ 2 | Bare minimum support for reading Microsoft PowerPoint files. 3 | 4 | History: 5 | Added February 19, 2025 6 | +/ 7 | module arsd.pptx; 8 | 9 | // see ~/zip/ppt 10 | 11 | import arsd.core; 12 | import arsd.zip; 13 | import arsd.dom; 14 | import arsd.color; 15 | 16 | /++ 17 | 18 | +/ 19 | class PptxFile { 20 | private ZipFile zipFile; 21 | private XmlDocument document; 22 | 23 | /++ 24 | 25 | +/ 26 | this(FilePath file) { 27 | this.zipFile = new ZipFile(file); 28 | 29 | load(); 30 | } 31 | 32 | /// ditto 33 | this(immutable(ubyte)[] rawData) { 34 | this.zipFile = new ZipFile(rawData); 35 | 36 | load(); 37 | } 38 | 39 | /// public for now but idk forever. 40 | PptxSlide[] slides; 41 | 42 | private string[string] contentTypes; 43 | private struct Relationship { 44 | string id; 45 | string type; 46 | string target; 47 | } 48 | private Relationship[string] relationships; 49 | 50 | private void load() { 51 | loadXml("[Content_Types].xml", (document) { 52 | foreach(element; document.querySelectorAll("Override")) 53 | contentTypes[element.attrs.PartName] = element.attrs.ContentType; 54 | }); 55 | loadXml("ppt/_rels/presentation.xml.rels", (document) { 56 | foreach(element; document.querySelectorAll("Relationship")) 57 | relationships[element.attrs.Id] = Relationship(element.attrs.Id, element.attrs.Type, element.attrs.Target); 58 | }); 59 | 60 | loadXml("ppt/presentation.xml", (document) { 61 | this.document = document; 62 | 63 | foreach(element; document.querySelectorAll("p\\:sldIdLst p\\:sldId")) 64 | loadXml("ppt/" ~ relationships[element.getAttribute("r:id")].target, (document) { 65 | slides ~= new PptxSlide(this, document); 66 | }); 67 | }); 68 | 69 | // then there's slide masters and layouts and idk what that is yet 70 | } 71 | 72 | private void loadXml(string filename, scope void delegate(XmlDocument document) handler) { 73 | auto document = new XmlDocument(cast(string) zipFile.getContent(filename)); 74 | handler(document); 75 | } 76 | 77 | } 78 | 79 | class PptxSlide { 80 | private PptxFile file; 81 | private XmlDocument document; 82 | private this(PptxFile file, XmlDocument document) { 83 | this.file = file; 84 | this.document = document; 85 | } 86 | 87 | /++ 88 | +/ 89 | string toPlainText() { 90 | // FIXME: need to handle at least some of the layout 91 | return document.root.innerText; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /reggaefile.d: -------------------------------------------------------------------------------- 1 | import reggae; 2 | 3 | enum commonFlags = "-w -g -debug"; 4 | 5 | alias default_ = dubDefaultTarget!( 6 | CompilerFlags(commonFlags), 7 | LinkerFlags(), 8 | CompilationMode.module_, 9 | ); 10 | 11 | alias ut = dubTestTarget!( 12 | CompilerFlags(commonFlags), 13 | LinkerFlags(), 14 | CompilationMode.module_, 15 | ); 16 | 17 | mixin build!(default_, ut); 18 | -------------------------------------------------------------------------------- /rtf.d: -------------------------------------------------------------------------------- 1 | /++ 2 | Some support for the RTF file format - rich text format, like produced by Windows WordPad. 3 | 4 | History: 5 | Added February 13, 2025 6 | +/ 7 | module arsd.rtf; 8 | 9 | // https://www.biblioscape.com/rtf15_spec.htm 10 | // https://latex2rtf.sourceforge.net/rtfspec_62.html 11 | // https://en.wikipedia.org/wiki/Rich_Text_Format 12 | 13 | // spacing is in "twips" or 1/20 of a point (as in text size unit). aka 1/1440th of an inch. 14 | 15 | import arsd.core; 16 | import arsd.color; 17 | 18 | /++ 19 | 20 | +/ 21 | struct RtfDocument { 22 | RtfGroup root; 23 | 24 | /++ 25 | There are two helper functions to process a RTF file: one that does minimal processing 26 | and sends you the data as it appears in the file, and one that sends you preprocessed 27 | results upon significant state changes. 28 | 29 | The former makes you do more work, but also exposes (almost) the whole file to you (it is still partially processed). The latter lets you just get down to business processing the text, but is not a complete implementation. 30 | +/ 31 | void process(void delegate(RtfPiece piece, ref RtfState state) dg) { 32 | recurseIntoGroup(root, RtfState.init, dg); 33 | } 34 | 35 | private static void recurseIntoGroup(RtfGroup group, RtfState parentState, void delegate(RtfPiece piece, ref RtfState state) dg) { 36 | // might need to copy... 37 | RtfState state = parentState; 38 | auto newDestination = group.destination; 39 | if(newDestination.length) 40 | state.currentDestination = newDestination; 41 | 42 | foreach(piece; group.pieces) { 43 | if(piece.contains == RtfPiece.Contains.group) { 44 | recurseIntoGroup(piece.group, state, dg); 45 | } else { 46 | dg(piece, state); 47 | } 48 | } 49 | 50 | } 51 | 52 | //Color[] colorTable; 53 | //Object[] fontTable; 54 | } 55 | 56 | /// ditto 57 | RtfDocument readRtfFromString(const(char)[] s) { 58 | return readRtfFromBytes(cast(const(ubyte)[]) s); 59 | } 60 | 61 | /// ditto 62 | RtfDocument readRtfFromBytes(const(ubyte)[] s) { 63 | RtfDocument document; 64 | 65 | if(s.length < 7) 66 | throw new ArsdException!"not a RTF file"("too short"); 67 | if((cast(char[]) s[0..6]) != `{\rtf1`) 68 | throw new ArsdException!"not a RTF file"("wrong magic number"); 69 | 70 | document.root = parseRtfGroup(s); 71 | 72 | return document; 73 | } 74 | 75 | /// ditto 76 | struct RtfState { 77 | string currentDestination; 78 | } 79 | 80 | unittest { 81 | auto document = readRtfFromString("{\\rtf1Hello\nWorld}"); 82 | //import std.file; auto document = readRtfFromString(readText("/home/me/test.rtf")); 83 | document.process((piece, ref state) { 84 | final switch(piece.contains) { 85 | case RtfPiece.Contains.controlWord: 86 | // writeln(state.currentDestination, ": ", piece.controlWord); 87 | break; 88 | case RtfPiece.Contains.text: 89 | // writeln(state.currentDestination, ": ", piece.text); 90 | break; 91 | case RtfPiece.Contains.group: 92 | assert(0); 93 | } 94 | }); 95 | 96 | // writeln(toPlainText(document)); 97 | } 98 | 99 | /++ 100 | Returns a plan text string that represents the jist of the document's content. 101 | +/ 102 | string toPlainText(RtfDocument document) { 103 | string ret; 104 | document.process((piece, ref state) { 105 | if(state.currentDestination.length) 106 | return; 107 | 108 | final switch(piece.contains) { 109 | case RtfPiece.Contains.controlWord: 110 | if(piece.controlWord.letterSequence == "par") 111 | ret ~= "\n\n"; 112 | else if(piece.controlWord.toDchar != dchar.init) 113 | ret ~= piece.controlWord.toDchar; 114 | break; 115 | case RtfPiece.Contains.text: 116 | ret ~= piece.text; 117 | break; 118 | case RtfPiece.Contains.group: 119 | assert(0); 120 | } 121 | }); 122 | 123 | return ret; 124 | } 125 | 126 | private RtfGroup parseRtfGroup(ref const(ubyte)[] s) { 127 | RtfGroup group; 128 | 129 | assert(s[0] == '{'); 130 | s = s[1 .. $]; 131 | if(s.length == 0) 132 | throw new ArsdException!"bad RTF file"("premature end after {"); 133 | while(s[0] != '}') { 134 | group.pieces ~= parseRtfPiece(s); 135 | if(s.length == 0) 136 | throw new ArsdException!"bad RTF file"("premature end before {"); 137 | } 138 | s = s[1 .. $]; 139 | return group; 140 | } 141 | 142 | private RtfPiece parseRtfPiece(ref const(ubyte)[] s) { 143 | while(true) 144 | switch(s[0]) { 145 | case '\\': 146 | return RtfPiece(parseRtfControlWord(s)); 147 | case '{': 148 | return RtfPiece(parseRtfGroup(s)); 149 | case '\t': 150 | s = s[1 .. $]; 151 | return RtfPiece(RtfControlWord.tab); 152 | case '\r': 153 | case '\n': 154 | // skip irrelevant characters 155 | s = s[1 .. $]; 156 | continue; 157 | default: 158 | return RtfPiece(parseRtfText(s)); 159 | } 160 | } 161 | 162 | private RtfControlWord parseRtfControlWord(ref const(ubyte)[] s) { 163 | assert(s[0] == '\\'); 164 | s = s[1 .. $]; 165 | 166 | if(s.length == 0) 167 | throw new ArsdException!"bad RTF file"("premature end after \\"); 168 | 169 | RtfControlWord ret; 170 | 171 | size_t pos; 172 | do { 173 | pos++; 174 | } while(pos < s.length && isAlpha(cast(char) s[pos])); 175 | 176 | ret.letterSequence = (cast(const char[]) s)[0 .. pos].idup; 177 | s = s[pos .. $]; 178 | 179 | if(isAlpha(ret.letterSequence[0])) { 180 | if(s.length == 0) 181 | throw new ArsdException!"bad RTF file"("premature end after control word"); 182 | 183 | int readNumber() { 184 | if(s.length == 0) 185 | throw new ArsdException!"bad RTF file"("premature end when reading number"); 186 | int count; 187 | while(s[count] >= '0' && s[count] <= '9') 188 | count++; 189 | if(count == 0) 190 | throw new ArsdException!"bad RTF file"("expected negative number, got something else"); 191 | 192 | auto buffer = cast(const(char)[]) s[0 .. count]; 193 | s = s[count .. $]; 194 | 195 | int accumulator; 196 | foreach(ch; buffer) { 197 | accumulator *= 10; 198 | accumulator += ch - '0'; 199 | } 200 | 201 | return accumulator; 202 | } 203 | 204 | if(s[0] == '-') { 205 | ret.hadNumber = true; 206 | s = s[1 .. $]; 207 | ret.number = - readNumber(); 208 | 209 | // negative number 210 | } else if(s[0] >= '0' && s[0] <= '9') { 211 | // non-negative number 212 | ret.hadNumber = true; 213 | ret.number = readNumber(); 214 | } 215 | 216 | if(s[0] == ' ') { 217 | ret.hadSpaceAtEnd = true; 218 | s = s[1 .. $]; 219 | } 220 | 221 | } else { 222 | // it was a control symbol 223 | if(ret.letterSequence == "\r" || ret.letterSequence == "\n") 224 | ret.letterSequence = "par"; 225 | } 226 | 227 | return ret; 228 | } 229 | 230 | private string parseRtfText(ref const(ubyte)[] s) { 231 | size_t end = s.length; 232 | foreach(idx, ch; s) { 233 | if(ch == '\\' || ch == '{' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '}') { 234 | end = idx; 235 | break; 236 | } 237 | } 238 | auto ret = s[0 .. end]; 239 | s = s[end .. $]; 240 | 241 | // FIXME: charset conversion? 242 | return (cast(const char[]) ret).idup; 243 | } 244 | 245 | // \r and \n chars w/o a \\ before them are ignored. but \ at the end of al ine is a \par 246 | // \t is read but you should use \tab generally 247 | // when reading, ima translate the ascii tab to \tab control word 248 | // and ignore 249 | /++ 250 | A union of entities you can see while parsing a RTF file. 251 | +/ 252 | struct RtfPiece { 253 | /++ 254 | +/ 255 | Contains contains() { 256 | return contains_; 257 | } 258 | /// ditto 259 | enum Contains { 260 | controlWord, 261 | group, 262 | text 263 | } 264 | 265 | this(RtfControlWord cw) { 266 | this.controlWord_ = cw; 267 | this.contains_ = Contains.controlWord; 268 | } 269 | this(RtfGroup g) { 270 | this.group_ = g; 271 | this.contains_ = Contains.group; 272 | } 273 | this(string s) { 274 | this.text_ = s; 275 | this.contains_ = Contains.text; 276 | } 277 | 278 | /++ 279 | +/ 280 | RtfControlWord controlWord() { 281 | if(contains != Contains.controlWord) 282 | throw ArsdException!"RtfPiece type mismatch"(contains); 283 | return controlWord_; 284 | } 285 | /++ 286 | +/ 287 | RtfGroup group() { 288 | if(contains != Contains.group) 289 | throw ArsdException!"RtfPiece type mismatch"(contains); 290 | return group_; 291 | } 292 | /++ 293 | +/ 294 | string text() { 295 | if(contains != Contains.text) 296 | throw ArsdException!"RtfPiece type mismatch"(contains); 297 | return text_; 298 | } 299 | 300 | private Contains contains_; 301 | 302 | private union { 303 | RtfControlWord controlWord_; 304 | RtfGroup group_; 305 | string text_; 306 | } 307 | } 308 | 309 | // a \word thing 310 | /++ 311 | A control word directly from the RTF file format. 312 | +/ 313 | struct RtfControlWord { 314 | bool hadSpaceAtEnd; 315 | bool hadNumber; 316 | string letterSequence; // what the word is 317 | int number; 318 | 319 | bool isDestination() { 320 | switch(letterSequence) { 321 | case 322 | "author", "comment", "subject", "title", 323 | "buptim", "creatim", "printim", "revtim", 324 | "doccomm", 325 | "footer", "footerf", "footerl", "footerr", 326 | "footnote", 327 | "ftncn", "ftnsep", "ftnsepc", 328 | "header", "headerf", "headerl", "headerr", 329 | "info", "keywords", "operator", 330 | "pict", 331 | "private", 332 | "rxe", 333 | "stylesheet", 334 | "tc", 335 | "txe", 336 | "xe": 337 | return true; 338 | case "colortbl": 339 | return true; 340 | case "fonttbl": 341 | return true; 342 | 343 | default: return false; 344 | } 345 | } 346 | 347 | dchar toDchar() { 348 | switch(letterSequence) { 349 | case "{": return '{'; 350 | case "}": return '}'; 351 | case `\`: return '\\'; 352 | case "~": return '\ '; 353 | case "tab": return '\t'; 354 | case "line": return '\n'; 355 | default: return dchar.init; 356 | } 357 | } 358 | 359 | bool isTurnOn() { 360 | return !hadNumber || number != 0; 361 | } 362 | 363 | // take no delimiters 364 | bool isControlSymbol() { 365 | // if true, the letterSequence is the symbol 366 | return letterSequence.length && !isAlpha(letterSequence[0]); 367 | } 368 | 369 | // letterSequence == ~ is a non breaking space 370 | 371 | static RtfControlWord tab() { 372 | RtfControlWord w; 373 | w.letterSequence = "tab"; 374 | return w; 375 | } 376 | } 377 | 378 | private bool isAlpha(char c) { 379 | return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); 380 | } 381 | 382 | // a { ... } thing 383 | /++ 384 | A group directly from the RTF file. 385 | +/ 386 | struct RtfGroup { 387 | RtfPiece[] pieces; 388 | 389 | string destination() { 390 | return isStarred() ? 391 | ((pieces.length > 1 && pieces[1].contains == RtfPiece.Contains.controlWord) ? pieces[1].controlWord.letterSequence : null) 392 | : ((pieces.length && pieces[0].contains == RtfPiece.Contains.controlWord && pieces[0].controlWord.isDestination) ? pieces[0].controlWord.letterSequence : null); 393 | } 394 | 395 | bool isStarred() { 396 | return (pieces.length && pieces[0].contains == RtfPiece.Contains.controlWord && pieces[0].controlWord.letterSequence == "*"); 397 | } 398 | } 399 | 400 | /+ 401 | \pard = paragraph defaults 402 | +/ 403 | -------------------------------------------------------------------------------- /sha.d: -------------------------------------------------------------------------------- 1 | /// Homemade SHA 1 and SHA 2 implementations. Beware of bugs - you should probably use [std.digest] instead. Kept more for historical curiosity than anything else. 2 | module arsd.sha; 3 | 4 | /* 5 | By Adam D. Ruppe, 26 Nov 2009 6 | I release this file into the public domain 7 | */ 8 | import std.stdio; 9 | 10 | version(GNU) { 11 | immutable(ubyte)[] SHA1(T)(T data) { 12 | import std.digest.sha; 13 | auto i = sha1Of(data); 14 | return i.idup; 15 | } 16 | 17 | immutable(ubyte)[] SHA256(T)(T data) { 18 | import std.digest.sha; 19 | auto i = sha256Of(data); 20 | return i.idup; 21 | } 22 | } else { 23 | 24 | immutable(ubyte)[/*20*/] SHA1(T)(T data) if(isInputRange!(T)) /*const(ubyte)[] data)*/ { 25 | uint[5] h = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]; 26 | 27 | SHARange!(T) range; 28 | static if(is(data == SHARange)) 29 | range = data; 30 | else { 31 | range.r = data; 32 | } 33 | /* 34 | ubyte[] message = data.dup; 35 | message ~= 0b1000_0000; 36 | while(((message.length+8) * 8) % 512) 37 | message ~= 0; 38 | 39 | ulong originalLength = cast(ulong) data.length * 8; 40 | 41 | for(int a = 7; a >= 0; a--) 42 | message ~= (originalLength >> (a*8)) & 0xff; // to big-endian 43 | 44 | assert(((message.length * 8) % 512) == 0); 45 | 46 | uint pos = 0; 47 | while(pos < message.length) { 48 | */ 49 | while(!range.empty) { 50 | uint[80] words; 51 | 52 | for(int a = 0; a < 16; a++) { 53 | for(int b = 3; b >= 0; b--) { 54 | words[a] |= cast(uint)(range.front()) << (b*8); 55 | range.popFront; 56 | // words[a] |= cast(uint)(message[pos]) << (b*8); 57 | // pos++; 58 | } 59 | } 60 | 61 | for(int a = 16; a < 80; a++) { 62 | uint t = words[a-3]; 63 | t ^= words[a-8]; 64 | t ^= words[a-14]; 65 | t ^= words[a-16]; 66 | asm { rol t, 1; } 67 | words[a] = t; 68 | } 69 | 70 | uint a = h[0]; 71 | uint b = h[1]; 72 | uint c = h[2]; 73 | uint d = h[3]; 74 | uint e = h[4]; 75 | 76 | for(int i = 0; i < 80; i++) { 77 | uint f, k; 78 | if(i >= 0 && i < 20) { 79 | f = (b & c) | ((~b) & d); 80 | k = 0x5A827999; 81 | } else 82 | if(i >= 20 && i < 40) { 83 | f = b ^ c ^ d; 84 | k = 0x6ED9EBA1; 85 | } else 86 | if(i >= 40 && i < 60) { 87 | f = (b & c) | (b & d) | (c & d); 88 | k = 0x8F1BBCDC; 89 | } else 90 | if(i >= 60 && i < 80) { 91 | f = b ^ c ^ d; 92 | k = 0xCA62C1D6; 93 | } else assert(0); 94 | 95 | uint temp; 96 | asm { 97 | mov EAX, a; 98 | rol EAX, 5; 99 | add EAX, f; 100 | add EAX, e; 101 | add EAX, k; 102 | mov temp, EAX; 103 | } 104 | temp += words[i]; 105 | e = d; 106 | d = c; 107 | asm { 108 | mov EAX, b; 109 | rol EAX, 30; 110 | mov c, EAX; 111 | } 112 | b = a; 113 | a = temp; 114 | } 115 | 116 | h[0] += a; 117 | h[1] += b; 118 | h[2] += c; 119 | h[3] += d; 120 | h[4] += e; 121 | } 122 | 123 | 124 | ubyte[] hash; 125 | for(int j = 0; j < 5; j++) 126 | for(int i = 3; i >= 0; i--) { 127 | hash ~= cast(ubyte)(h[j] >> (i*8))&0xff; 128 | } 129 | 130 | return hash.idup; 131 | } 132 | 133 | import core.stdc.stdio; 134 | import std.string; 135 | // i wish something like this was in phobos. 136 | struct FileByByte { 137 | FILE* fp; 138 | this(string filename) { 139 | fp = fopen(toStringz(filename), "rb".ptr); 140 | if(fp is null) 141 | throw new Exception("couldn't open " ~ filename); 142 | popFront(); 143 | } 144 | 145 | // FIXME: this should prolly be recounted or something. blargh. 146 | 147 | ~this() { 148 | if(fp !is null) 149 | fclose(fp); 150 | } 151 | 152 | void popFront() { 153 | f = cast(ubyte) fgetc(fp); 154 | } 155 | 156 | @property ubyte front() { 157 | return f; 158 | } 159 | 160 | @property bool empty() { 161 | return feof(fp) ? true : false; 162 | } 163 | 164 | ubyte f; 165 | } 166 | 167 | import std.range; 168 | 169 | // This does the preprocessing of input data, fetching one byte at a time of the data until it is empty, then the padding and length at the end 170 | template SHARange(T) if(isInputRange!(T)) { 171 | struct SHARange { 172 | T r; 173 | 174 | bool empty() { 175 | return state == 5; 176 | } 177 | 178 | void popFront() { 179 | if(state == 0) { 180 | r.popFront; 181 | /* 182 | static if(__traits(compiles, r.front.length)) 183 | length += r.front.length; 184 | else 185 | length += r.front().sizeof; 186 | */ 187 | length++; // FIXME 188 | 189 | if(r.empty) { 190 | state = 1; 191 | position = 2; 192 | current = 0x80; 193 | } 194 | } else { 195 | bool hackforward = false; 196 | if(state == 1) { 197 | current = 0x0; 198 | state = 2; 199 | if((((position + length + 8) * 8) % 512) == 8) { 200 | position--; 201 | hackforward = true; 202 | } 203 | goto proceed; 204 | // position++; 205 | } else if( state == 2) { 206 | proceed: 207 | if(!(((position + length + 8) * 8) % 512)) { 208 | state = 3; 209 | position = 7; 210 | length *= 8; 211 | if(hackforward) 212 | goto proceedmoar; 213 | } else 214 | position++; 215 | } else if (state == 3) { 216 | proceedmoar: 217 | current = (length >> (position*8)) & 0xff; 218 | if(position == 0) 219 | state = 4; 220 | else 221 | position--; 222 | } else if (state == 4) { 223 | current = 0xff; 224 | state = 5; 225 | } 226 | } 227 | } 228 | 229 | ubyte front() { 230 | if(state == 0) { 231 | return cast(ubyte) r.front(); 232 | } 233 | assert(state != 5); 234 | //writefln("%x", current); 235 | return current; 236 | } 237 | 238 | ubyte current; 239 | uint position; 240 | ulong length; 241 | int state = 0; // reading range, reading appended bit, reading padding, reading length, done 242 | } 243 | } 244 | 245 | immutable(ubyte)[] SHA256(T)(T data) if ( isInputRange!(T)) { 246 | uint[8] h = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]; 247 | immutable(uint[64]) k = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 248 | 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 249 | 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 250 | 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 251 | 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 252 | 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 253 | 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 254 | 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]; 255 | 256 | SHARange!(T) range; 257 | static if(is(data == SHARange)) 258 | range = data; 259 | else { 260 | range.r = data; 261 | } 262 | /* 263 | ubyte[] message = cast(ubyte[]) data.dup; 264 | message ~= 0b1000_0000; 265 | while(((message.length+8) * 8) % 512) 266 | message ~= 0; 267 | 268 | ulong originalLength = cast(ulong) data.length * 8; 269 | 270 | for(int a = 7; a >= 0; a--) 271 | message ~= (originalLength >> (a*8)) & 0xff; // to big-endian 272 | 273 | assert(((message.length * 8) % 512) == 0); 274 | */ 275 | // uint pos = 0; 276 | while(!range.empty) { 277 | // while(pos < message.length) { 278 | uint[64] words; 279 | 280 | for(int a = 0; a < 16; a++) { 281 | for(int b = 3; b >= 0; b--) { 282 | words[a] |= cast(uint)(range.front()) << (b*8); 283 | //words[a] |= cast(uint)(message[pos]) << (b*8); 284 | range.popFront; 285 | // pos++; 286 | } 287 | } 288 | 289 | for(int a = 16; a < 64; a++) { 290 | uint t1 = words[a-15]; 291 | asm { 292 | mov EAX, t1; 293 | mov EBX, EAX; 294 | mov ECX, EAX; 295 | ror EAX, 7; 296 | ror EBX, 18; 297 | shr ECX, 3; 298 | xor EAX, EBX; 299 | xor EAX, ECX; 300 | mov t1, EAX; 301 | } 302 | uint t2 = words[a-2]; 303 | asm { 304 | mov EAX, t2; 305 | mov EBX, EAX; 306 | mov ECX, EAX; 307 | ror EAX, 17; 308 | ror EBX, 19; 309 | shr ECX, 10; 310 | xor EAX, EBX; 311 | xor EAX, ECX; 312 | mov t2, EAX; 313 | } 314 | 315 | words[a] = words[a-16] + t1 + words[a-7] + t2; 316 | } 317 | 318 | uint A = h[0]; 319 | uint B = h[1]; 320 | uint C = h[2]; 321 | uint D = h[3]; 322 | uint E = h[4]; 323 | uint F = h[5]; 324 | uint G = h[6]; 325 | uint H = h[7]; 326 | 327 | for(int i = 0; i < 64; i++) { 328 | uint s0; 329 | asm { 330 | mov EAX, A; 331 | mov EBX, EAX; 332 | mov ECX, EAX; 333 | ror EAX, 2; 334 | ror EBX, 13; 335 | ror ECX, 22; 336 | xor EAX, EBX; 337 | xor EAX, ECX; 338 | mov s0, EAX; 339 | } 340 | uint maj = (A & B) ^ (A & C) ^ (B & C); 341 | uint t2 = s0 + maj; 342 | uint s1; 343 | asm { 344 | mov EAX, E; 345 | mov EBX, EAX; 346 | mov ECX, EAX; 347 | ror EAX, 6; 348 | ror EBX, 11; 349 | ror ECX, 25; 350 | xor EAX, EBX; 351 | xor EAX, ECX; 352 | mov s1, EAX; 353 | } 354 | uint ch = (E & F) ^ ((~E) & G); 355 | uint t1 = H + s1 + ch + k[i] + words[i]; 356 | 357 | H = G; 358 | G = F; 359 | F = E; 360 | E = D + t1; 361 | D = C; 362 | C = B; 363 | B = A; 364 | A = t1 + t2; 365 | } 366 | 367 | h[0] += A; 368 | h[1] += B; 369 | h[2] += C; 370 | h[3] += D; 371 | h[4] += E; 372 | h[5] += F; 373 | h[6] += G; 374 | h[7] += H; 375 | } 376 | 377 | ubyte[] hash; 378 | for(int j = 0; j < 8; j++) 379 | for(int i = 3; i >= 0; i--) { 380 | hash ~= cast(ubyte)(h[j] >> (i*8))&0xff; 381 | } 382 | 383 | return hash.idup; 384 | } 385 | 386 | } 387 | 388 | import std.exception; 389 | 390 | string hashToString(const(ubyte)[] hash) { 391 | char[] s; 392 | 393 | s.length = hash.length * 2; 394 | 395 | char toHex(int a) { 396 | if(a < 10) 397 | return cast(char) (a + '0'); 398 | else 399 | return cast(char) (a + 'a' - 10); 400 | } 401 | 402 | for(int a = 0; a < hash.length; a++) { 403 | s[a*2] = toHex(hash[a] >> 4); 404 | s[a*2+1] = toHex(hash[a] & 0x0f); 405 | } 406 | 407 | return assumeUnique(s); 408 | } 409 | /* 410 | string tee(string t) { 411 | writefln("%s", t); 412 | return t; 413 | } 414 | */ 415 | unittest { 416 | assert(hashToString(SHA1("abc")) == "a9993e364706816aba3e25717850c26c9cd0d89d"); 417 | assert(hashToString(SHA1("sdfj983yr2ih")) == "335f1f5a4af4aa2c8e93b88d69dda2c22baeb94d"); 418 | assert(hashToString(SHA1("$%&^54ylkufg09fd7f09sa7udsiouhcx987yw98etf7yew98yfds987f632uw90ruds09fudsf09dsuhfoidschyds98fydovipsdaidsd9fsa GA UIA duisguifgsuifgusaufisgfuisafguisagasuidgsaufsauifhuisahfuisafaoisahasiosafhffdasasdisayhfdoisayf8saiuhgduifyds8fiydsufisafoisayf8sayfd98wqyr98wqy98sayd98sayd098sayd09sayd98sayd98saicxyhckxnvjbpovc pousa09cusa 09csau csa9 dusa90d usa9d0sau dsa90 as09posufpodsufodspufdspofuds 9tu sapfusaa daosjdoisajdsapoihdsaiodyhsaioyfg d98ytewq89rysa 98yc98sdxych sa89ydsa89dy sa98ydas98c ysx9v8y cxv89ysd f8ysa89f ysa89fd sg8yhds9g8 rfjcxhvslkhdaiosy09wq7r987t98e7ys98aIYOIYOIY)(*YE (*WY *A(YSA* HDUIHDUIAYT&*ATDAUID AUI DUIAT DUIAG saoidusaoid ysqoid yhsaduiayh UIZYzuI YUIYEDSA UIDYUIADYISA YTDGS UITGUID")) == "e38a1220eaf8103d6176df2e0dd0a933e2f52001"); 419 | 420 | assert(hashToString(SHA256("abc")) == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); 421 | assert(hashToString(SHA256("$%&^54ylkufg09fd7f09sa7udsiouhcx987yw98etf7yew98yfds987f632uw90ruds09fudsf09dsuhfoidschyds98fydovipsdaidsd9fsa GA UIA duisguifgsuifgusaufisgfuisafguisagasuidgsaufsauifhuisahfuisafaoisahasiosafhffdasasdisayhfdoisayf8saiuhgduifyds8fiydsufisafoisayf8sayfd98wqyr98wqy98sayd98sayd098sayd09sayd98sayd98saicxyhckxnvjbpovc pousa09cusa 09csau csa9 dusa90d usa9d0sau dsa90 as09posufpodsufodspufdspofuds 9tu sapfusaa daosjdoisajdsapoihdsaiodyhsaioyfg d98ytewq89rysa 98yc98sdxych sa89ydsa89dy sa98ydas98c ysx9v8y cxv89ysd f8ysa89f ysa89fd sg8yhds9g8 rfjcxhvslkhdaiosy09wq7r987t98e7ys98aIYOIYOIY)(*YE (*WY *A(YSA* HDUIHDUIAYT&*ATDAUID AUI DUIAT DUIAG saoidusaoid ysqoid yhsaduiayh UIZYzuI YUIYEDSA UIDYUIADYISA YTDGS UITGUID")) == "64ff79c67ad5ddf9ba5b2d83e07a6937ef9a5b4eb39c54fe1e913e21aad0e95c"); 422 | } 423 | /* 424 | void main() { 425 | auto hash = SHA256(InputByChar(stdin)); 426 | writefln("%s", hashToString(hash)); 427 | } 428 | */ 429 | -------------------------------------------------------------------------------- /sslsocket.d: -------------------------------------------------------------------------------- 1 | /* * 2 | 3 | Don't use this file anymore. The maintained version is in http2.d, just use that. 4 | 5 | Old docs below: 6 | 7 | This is CLIENT only at this point. Don't try to 8 | bind/accept with these. 9 | 10 | FIXME: Windows isn't implemented 11 | 12 | On Windows, it uses Microsoft schannel so it doesn't 13 | need openssl or gnutls as a dependency. 14 | 15 | On other platforms, it uses the openssl api, which should 16 | work with both openssl and gnutls. 17 | 18 | 19 | btw, interesting: 20 | http://msdn.microsoft.com/en-us/library/windows/desktop/aa364510%28v=vs.85%29.aspx 21 | */ 22 | module sslsocket; 23 | 24 | 25 | import std.socket; 26 | 27 | // see also: 28 | // http://msdn.microsoft.com/en-us/library/aa380536%28v=vs.85%29.aspx 29 | 30 | // import deimos.openssl.ssl; 31 | 32 | version=use_openssl; 33 | 34 | version(use_openssl) { 35 | alias SslClientSocket = OpenSslSocket; 36 | 37 | extern(C) { 38 | int SSL_library_init(); 39 | void OpenSSL_add_all_ciphers(); 40 | void OpenSSL_add_all_digests(); 41 | void SSL_load_error_strings(); 42 | 43 | struct SSL {} 44 | struct SSL_CTX {} 45 | struct SSL_METHOD {} 46 | 47 | SSL_CTX* SSL_CTX_new(const SSL_METHOD* method); 48 | SSL* SSL_new(SSL_CTX*); 49 | int SSL_pending(SSL*); 50 | int SSL_set_fd(SSL*, int); 51 | int SSL_connect(SSL*); 52 | int SSL_write(SSL*, const void*, int); 53 | int SSL_read(SSL*, void*, int); 54 | void SSL_free(SSL*); 55 | void SSL_CTX_free(SSL_CTX*); 56 | 57 | void SSL_set_verify(SSL*, int, void*); 58 | enum SSL_VERIFY_NONE = 0; 59 | 60 | SSL_METHOD* SSLv3_client_method(); 61 | SSL_METHOD* TLS_client_method(); 62 | SSL_METHOD* SSLv23_client_method(); 63 | 64 | void ERR_print_errors_fp(FILE*); 65 | } 66 | 67 | import core.stdc.stdio; 68 | 69 | shared static this() { 70 | SSL_library_init(); 71 | OpenSSL_add_all_ciphers(); 72 | OpenSSL_add_all_digests(); 73 | SSL_load_error_strings(); 74 | } 75 | 76 | pragma(lib, "crypto"); 77 | pragma(lib, "ssl"); 78 | 79 | class OpenSslSocket : Socket { 80 | private SSL* ssl; 81 | private SSL_CTX* ctx; 82 | private void initSsl(bool verifyPeer) { 83 | ctx = SSL_CTX_new(SSLv23_client_method()); 84 | assert(ctx !is null); 85 | 86 | ssl = SSL_new(ctx); 87 | if(!verifyPeer) 88 | SSL_set_verify(ssl, SSL_VERIFY_NONE, null); 89 | SSL_set_fd(ssl, cast(int) this.handle); 90 | } 91 | 92 | bool dataPending() { 93 | return SSL_pending(ssl) > 0; 94 | } 95 | 96 | @trusted 97 | override void connect(Address to) { 98 | super.connect(to); 99 | if(SSL_connect(ssl) == -1) { 100 | ERR_print_errors_fp(stderr); 101 | int i; 102 | printf("wtf\n"); 103 | scanf("%d\n", &i); 104 | throw new Exception("ssl connect"); 105 | } 106 | } 107 | 108 | @trusted 109 | override ptrdiff_t send(scope const(void)[] buf, SocketFlags flags) { 110 | auto retval = SSL_write(ssl, buf.ptr, cast(uint) buf.length); 111 | if(retval == -1) { 112 | ERR_print_errors_fp(stderr); 113 | int i; 114 | printf("wtf\n"); 115 | scanf("%d\n", &i); 116 | throw new Exception("ssl send"); 117 | } 118 | return retval; 119 | 120 | } 121 | override ptrdiff_t send(scope const(void)[] buf) { 122 | return send(buf, SocketFlags.NONE); 123 | } 124 | @trusted 125 | override ptrdiff_t receive(scope void[] buf, SocketFlags flags) { 126 | auto retval = SSL_read(ssl, buf.ptr, cast(int)buf.length); 127 | if(retval == -1) { 128 | ERR_print_errors_fp(stderr); 129 | int i; 130 | printf("wtf\n"); 131 | scanf("%d\n", &i); 132 | throw new Exception("ssl send"); 133 | } 134 | return retval; 135 | } 136 | override ptrdiff_t receive(scope void[] buf) { 137 | return receive(buf, SocketFlags.NONE); 138 | } 139 | 140 | this(AddressFamily af, SocketType type = SocketType.STREAM, bool verifyPeer = true) { 141 | super(af, type); 142 | initSsl(verifyPeer); 143 | } 144 | 145 | this(socket_t sock, AddressFamily af) { 146 | super(sock, af); 147 | initSsl(true); 148 | } 149 | 150 | ~this() { 151 | SSL_free(ssl); 152 | SSL_CTX_free(ctx); 153 | } 154 | } 155 | } 156 | 157 | version(ssl_test) 158 | void main() { 159 | auto sock = new SslClientSocket(AddressFamily.INET); 160 | sock.connect(new InternetAddress("localhost", 443)); 161 | sock.send("GET / HTTP/1.0\r\n\r\n"); 162 | import std.stdio; 163 | char[1024] buffer; 164 | writeln(buffer[0 .. sock.receive(buffer)]); 165 | } 166 | -------------------------------------------------------------------------------- /string.d: -------------------------------------------------------------------------------- 1 | /++ 2 | String manipulation functions. 3 | 4 | See_Also: 5 | To get a substring, you can use the built-in array slice operator. 6 | 7 | For reading various encodings into a standard string, see [arsd.characterencodings]. 8 | 9 | For converting things to and from strings, see [arsd.conv]. 10 | 11 | For sorting an array of strings, see... std.algorithm for now but maybe here later. 12 | 13 | History: 14 | Added May 23, 2025 15 | +/ 16 | module arsd.string; 17 | 18 | static import arsd.core; 19 | 20 | /// Public interface to arsd.core 21 | alias startsWith = arsd.core.startsWith; 22 | /// ditto 23 | alias endsWith = arsd.core.endsWith; 24 | /// ditto 25 | alias indexOf = arsd.core.indexOf; 26 | 27 | // replace? replaceFirst, replaceAll, replaceAny etc 28 | 29 | // limitSize - truncates to the last code point under the given length of code units 30 | 31 | /// Strips (aka trims) leading and/or trailing whitespace from the string. 32 | alias strip = arsd.core.stripInternal; 33 | /// ditto 34 | deprecated("D calls this `strip` instead") alias trim = strip; 35 | 36 | /// ditto 37 | alias stripRight = arsd.core.stripInternal; 38 | /// ditto 39 | deprecated("D calls this `stripRight` instead") alias trimRight = stripRight; 40 | 41 | // stripLeft? variants where you can list the chars to strip? 42 | 43 | // ascii to upper, to lower, capitalize words, from camel case to dash separated 44 | 45 | // ********* UTF ************** 46 | // utf8 stride and such? 47 | // get the starting code unit of the given point in the string 48 | // get the next code unit start after the given point (compare upstream popFront) 49 | // iterate over a string putting a replacement char in any invalid utf 8 spot 50 | 51 | // ********* C INTEROP ************** 52 | 53 | alias stringz = arsd.core.stringz; 54 | // CharzBuffer 55 | // WCharzBuffer 56 | -------------------------------------------------------------------------------- /uda.d: -------------------------------------------------------------------------------- 1 | /++ 2 | 3 | +/ 4 | module arsd.uda; 5 | 6 | /++ 7 | 8 | +/ 9 | Blueprint extractUdas(Blueprint, Udas...)(Blueprint defaults) { 10 | foreach(alias uda; Udas) { 11 | static if(is(typeof(uda) == Blueprint)) { 12 | defaults = uda; 13 | } else { 14 | foreach(ref member; defaults.tupleof) 15 | static if(is(typeof(member) == typeof(uda))) 16 | member = uda; 17 | } 18 | } 19 | 20 | return defaults; 21 | } 22 | 23 | unittest { 24 | import core.attribute; 25 | static struct Name { 26 | @implicit this(string name) { this.name = name; } 27 | string name; 28 | } 29 | 30 | static struct Priority { 31 | @implicit this(int priority) { this.priority = priority; } 32 | int priority; 33 | } 34 | 35 | static struct Blueprint { 36 | Name name; 37 | Priority priority; 38 | } 39 | 40 | static class A { 41 | @Name("a") int a; 42 | @Priority(44) int b; 43 | int c; 44 | @Priority(33) @Name("d") int d; 45 | // @(wtf => wtf) int e; // won't compile when trying to get the blueprint... 46 | 47 | @Blueprint(name: "foo", priority: 44) int g; 48 | } 49 | 50 | auto bp2 = Blueprint(name: "foo", priority: 44); 51 | 52 | foreach(memberName; __traits(derivedMembers, A)) { 53 | alias member = __traits(getMember, A, memberName); 54 | auto bp = extractUdas!(Blueprint, __traits(getAttributes, member))(Blueprint.init); 55 | import std.stdio; writeln(memberName, " ", bp); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /uri.d: -------------------------------------------------------------------------------- 1 | /++ 2 | Future public interface to the Uri struct and encode/decode component functions. 3 | 4 | History: 5 | Added May 26, 2025 6 | +/ 7 | module arsd.uri; 8 | 9 | import arsd.core; 10 | 11 | alias encodeUriComponent = arsd.core.encodeUriComponent; 12 | alias decodeUriComponent = arsd.core.decodeUriComponent; 13 | 14 | // phobos compatibility names 15 | alias encodeComponent = encodeUriComponent; 16 | alias decodeComponent = decodeUriComponent; 17 | 18 | // FIXME: merge and pull Uri struct from http2 and cgi. maybe via core. 19 | 20 | // might also put base64 in here.... 21 | -------------------------------------------------------------------------------- /wmutil.d: -------------------------------------------------------------------------------- 1 | /++ 2 | Cross platform window manager utilities for interacting with other unknown windows on the OS. 3 | 4 | Based on [arsd.simpledisplay]. 5 | +/ 6 | module arsd.wmutil; 7 | 8 | public import arsd.simpledisplay; 9 | 10 | version(Windows) 11 | import core.sys.windows.windows; 12 | 13 | static assert(UsingSimpledisplayX11 || UsingSimpledisplayWindows, "wmutil only works on X11 or Windows"); 14 | 15 | static if (UsingSimpledisplayX11) { 16 | extern(C) nothrow @nogc { 17 | Atom* XListProperties(Display *display, Window w, int *num_prop_return); 18 | Status XGetTextProperty(Display *display, Window w, XTextProperty *text_prop_return, Atom property); 19 | Status XQueryTree(Display *display, Window w, Window *root_return, Window *parent_return, Window **children_return, uint *nchildren_return); 20 | } 21 | } 22 | 23 | /// A foreachable object that iterates window children 24 | struct WindowChildrenIterator { 25 | NativeWindowHandle parent; 26 | 27 | version(Windows) 28 | struct EnumParams { 29 | int result; 30 | int delegate(NativeWindowHandle) dg; 31 | Exception ex; 32 | } 33 | 34 | 35 | 36 | version(Windows) 37 | extern(Windows) 38 | nothrow private static int helper(HWND window, LPARAM lparam) { 39 | EnumParams* args = cast(EnumParams*)lparam; 40 | try { 41 | args.result = args.dg(window); 42 | if (args.result) 43 | return 0; 44 | else 45 | return 1; 46 | } catch (Exception e) { 47 | args.ex = e; 48 | return 0; 49 | } 50 | } 51 | 52 | /// 53 | int opApply(int delegate(NativeWindowHandle) dg) const { 54 | version (Windows) { 55 | EnumParams params; 56 | 57 | // the cast is cuz druntime seems to have a wrong definition here, missing the const 58 | EnumChildWindows(cast(void*) parent, &helper, cast(LPARAM)¶ms); 59 | 60 | if (params.ex) 61 | throw params.ex; 62 | 63 | return params.result; 64 | } else static if (UsingSimpledisplayX11) { 65 | int result; 66 | Window unusedWindow; 67 | Window* children; 68 | uint numChildren; 69 | Status status = XQueryTree(XDisplayConnection.get(), parent, &unusedWindow, &unusedWindow, &children, &numChildren); 70 | if (status == 0 || children is null) 71 | return 0; 72 | scope (exit) 73 | XFree(children); 74 | 75 | foreach (window; children[0 .. numChildren]) { 76 | result = dg(window); 77 | if (result) 78 | break; 79 | } 80 | return result; 81 | } else 82 | static assert(0); 83 | 84 | } 85 | } 86 | 87 | /// 88 | WindowChildrenIterator iterateWindows(NativeWindowHandle parent = NativeWindowHandle.init) { 89 | static if (UsingSimpledisplayX11) 90 | if (parent == NativeWindowHandle.init) 91 | parent = RootWindow(XDisplayConnection.get, DefaultScreen(XDisplayConnection.get)); 92 | 93 | return WindowChildrenIterator(parent); 94 | } 95 | 96 | /++ 97 | Searches for a window with the specified class name and returns the native window handle to it. 98 | 99 | Params: 100 | className = the class name to check the window for, case-insensitive. 101 | +/ 102 | NativeWindowHandle findWindowByClass(string className) { 103 | version (Windows) 104 | return findWindowByClass(className.toWStringz); 105 | else static if (UsingSimpledisplayX11) { 106 | import std.algorithm : splitter; 107 | import std.uni : sicmp; 108 | 109 | auto classAtom = GetAtom!"WM_CLASS"(XDisplayConnection.get()); 110 | Atom returnType; 111 | int returnFormat; 112 | arch_ulong numItems, bytesAfter; 113 | char* strs; 114 | foreach (window; iterateWindows) { 115 | if (0 == XGetWindowProperty(XDisplayConnection.get(), window, classAtom, 0, 64, false, AnyPropertyType, &returnType, &returnFormat, &numItems, &bytesAfter, cast(void**)&strs)) { 116 | scope (exit) 117 | XFree(strs); 118 | if (returnFormat == 8) { 119 | foreach (windowClassName; strs[0 .. numItems].splitter('\0')) { 120 | if (sicmp(windowClassName, className) == 0) 121 | return window; 122 | } 123 | } 124 | } 125 | } 126 | return NativeWindowHandle.init; 127 | 128 | } 129 | } 130 | 131 | /// ditto 132 | version (Windows) 133 | NativeWindowHandle findWindowByClass(LPCTSTR className) { 134 | return FindWindow(className, null); 135 | } 136 | 137 | /++ 138 | Get the PID that owns the window. 139 | 140 | Params: 141 | window = The window to check who created it 142 | Returns: the PID of the owner who created this window. On windows this will always work and be accurate. On X11 this might return -1 if none is specified and might not actually be the actual owner. 143 | +/ 144 | int ownerPID(NativeWindowHandle window) @property { 145 | version (Windows) { 146 | DWORD ret; 147 | GetWindowThreadProcessId(window, &ret); 148 | return cast(int) ret; 149 | } else static if (UsingSimpledisplayX11) { 150 | auto pidAtom = GetAtom!"_NET_WM_PID"(XDisplayConnection.get()); 151 | Atom returnType; 152 | int returnFormat; 153 | arch_ulong numItems, bytesAfter; 154 | uint* ints; 155 | if (0 == XGetWindowProperty(XDisplayConnection.get(), window, pidAtom, 0, 1, false, AnyPropertyType, &returnType, &returnFormat, &numItems, &bytesAfter, cast(void**)&ints)) { 156 | scope (exit) 157 | XFree(ints); 158 | if (returnFormat < 64 && numItems > 0) { 159 | return *ints; 160 | } 161 | } 162 | return -1; 163 | } 164 | } 165 | 166 | unittest { 167 | import std.stdio; 168 | auto window = findWindowByClass("x-terminal-emulator"); 169 | writeln("Terminal: ", window.ownerPID); 170 | foreach (w; iterateWindows) 171 | writeln(w.ownerPID); 172 | } 173 | -------------------------------------------------------------------------------- /xwindows.d: -------------------------------------------------------------------------------- 1 | /** 2 | This module is obsolete. Its functionality has been merged 3 | into simpledisplay.d. You can use it instead and abandon this. 4 | 5 | 6 | Old stuff follows: 7 | 8 | This module is a bunch of helper functions for dealing with X11 9 | windows on top of simpledisplay.d 10 | 11 | It is mostly about the atoms that communicate stuff to things like 12 | window managers and taskbars. 13 | 14 | The eventual goal is for this to be useful for writing those and for 15 | writing plain applications. 16 | */ 17 | module arsd.xwindows; 18 | 19 | public import arsd.simpledisplay; 20 | 21 | -------------------------------------------------------------------------------- /zip.d: -------------------------------------------------------------------------------- 1 | /++ 2 | DO NOT USE - ZERO STABILITY AT THIS TIME. 3 | 4 | Support for reading (and later, writing) .zip files. 5 | 6 | Currently a wrapper around phobos to change the interface for consistency 7 | and compatibility with my other modules. 8 | 9 | You're better off using Phobos [std.zip] for stability at this time. 10 | 11 | History: 12 | Added February 19, 2025 13 | +/ 14 | module arsd.zip; 15 | 16 | import arsd.core; 17 | 18 | import std.zip; 19 | 20 | // https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT 21 | 22 | /++ 23 | 24 | +/ 25 | class ZipFile { 26 | ZipArchive phobos; 27 | 28 | /++ 29 | 30 | +/ 31 | this(immutable(ubyte)[] fileData) { 32 | phobos = new ZipArchive(cast(void[]) fileData); 33 | } 34 | 35 | /// ditto 36 | this(FilePath filename) { 37 | import std.file; 38 | this(cast(immutable(ubyte)[]) std.file.read(filename.toString())); 39 | } 40 | 41 | /++ 42 | Unstable, avoid. 43 | +/ 44 | immutable(ubyte)[] getContent(string filename, bool allowEmptyIfNotExist = false) { 45 | if(filename !in phobos.directory) { 46 | if(allowEmptyIfNotExist) 47 | return null; 48 | throw ArsdException!"Zip content not found"(filename); 49 | } 50 | return cast(immutable(ubyte)[]) phobos.expand(phobos.directory[filename]); 51 | } 52 | } 53 | --------------------------------------------------------------------------------