Compare commits

...

238 Commits

Author SHA1 Message Date
Adam D. Ruppe a36955032d
Merge pull request #490 from naydef/master
Make buttons disableable for custom drawn buttons
2025-04-10 07:33:55 -04:00
Adam D. Ruppe 98e080d34d handle emails with attachments before main body 2025-04-08 14:55:43 -04:00
naydef bd91ca8b01
Method 1 for redrawing 2025-04-08 18:05:45 +00:00
naydef 45daf12ab1
Make buttons disableable 2025-04-08 13:58:32 +00:00
Adam D. Ruppe f1a259ecac more stuff im too lazy to figure out details 2025-03-29 21:05:17 -04:00
Adam D. Ruppe d1cb09bdaa more window types 2025-03-24 14:56:09 -04:00
Adam D. Ruppe e18822c432
Merge pull request #489 from VPanteleev-S7/fast-to-lower
[RFC] arsd.postgres: Add fast path for field name conversion
2025-03-23 16:39:53 -04:00
Vladimir Panteleev 7a4cb05709 arsd.postgres: Add fast path for field name conversion
Skip the slow std.uni.toLower call unless it's necessary.
2025-03-23 20:37:54 +00:00
Adam D. Ruppe 4fb3ea691d
Merge pull request #487 from kinke/dont_link_curl
arsd.email: Don't needlessly link libcurl
2025-03-11 12:41:51 -04:00
Martin Kinkelin 191bac9b12 arsd.email: Don't needlessly link libcurl 2025-03-11 17:33:07 +01:00
Adam D. Ruppe 269b535196 lol 32 bit 2025-02-22 12:08:56 -05:00
Adam D. Ruppe 2a065c3a27 dom stream enhancements 2025-02-22 11:08:24 -05:00
Adam D. Ruppe 61a5698394 lots of things in prep for blog 2025-02-20 20:04:11 -05:00
Adam D. Ruppe 31fa714504
Merge pull request #478 from analogjupiter/ini
Add `arsd.ini` module
2025-02-16 20:44:55 -05:00
Elias Batek 5d31192edb Add `parseIniMergedAA` example 2025-02-16 21:35:39 +01:00
Elias Batek e29d8fcd22 Improve getting started section of `arsd.ini` 2025-02-16 21:31:25 +01:00
Elias Batek fd5dab8c43 Move `arsd.ini` changelog entry to v12
Adam said the v12 release hasn’t happened yet.
2025-02-16 01:09:17 +01:00
Elias Batek c38b37cce9 Implement function `parseIniMergedAA` 2025-02-16 01:04:53 +01:00
Elias Batek c300956cf7 Make `stringifyIni` overloads public 2025-02-16 00:43:19 +01:00
Elias Batek 6f59ff160c Link `IniDocument` in getting started section 2025-02-15 22:16:50 +01:00
Elias Batek cb781b853d Add additional “getting started” section 2025-02-15 21:45:12 +01:00
Elias Batek b9ea9562fc Clarify `immutable(char)[]` string type 2025-02-15 21:31:57 +01:00
Elias Batek 2aa7a7573c Add `parseIniDocument` example 2025-02-14 04:38:16 +01:00
Elias Batek 7e793993b9 Add `writeIniFile` example 2025-02-14 04:24:13 +01:00
Elias Batek 007a637559 Update sub-package description in DUB recipe 2025-02-14 04:20:13 +01:00
Elias Batek a1a96a44cd Implement INI string serializer
Ued to “stringify” INI documents.
2025-02-14 04:17:27 +01:00
Adam D. Ruppe 644c1869a1 logger framework almost actually usable 2025-02-13 09:15:06 -05:00
Elias Batek 433593db48 Fix known bug with line-folding keys or sections 2025-02-13 06:29:27 +01:00
Elias Batek c9198a4e79 Mention `arsd.ini` in README 2025-02-13 05:49:30 +01:00
Elias Batek aae2418f05 Merge branch 'master' into ini 2025-02-13 05:44:26 +01:00
Elias Batek 08f9ba3c95 Implement escape sequences + line folding 2025-02-13 05:40:03 +01:00
Elias Batek 723fa5be40 Fix filtering of first token in `IniFilteredParser` 2025-02-13 02:58:01 +01:00
Elias Batek 88b50feef1 Refactor AA to have keys typed as `immutable(char)[]` 2025-02-12 02:40:51 +01:00
Elias Batek a2fe6f1fb4 Remove destruciveness footguns and add further documentation 2025-02-12 02:13:34 +01:00
Elias Batek 5c7538421f Chomp chomp 2025-02-12 01:49:32 +01:00
Elias Batek eacf798788 Rename `mut` and `dup` to `destructive` and `nonDestructive` 2025-02-12 01:07:18 +01:00
Elias Batek 51d51e5a98 Implement `Dialect.concatSubstrings` in low-level `IniParser` 2025-02-11 06:35:08 +01:00
Elias Batek 5a3a16a150 Fix template instantiation bug 2025-02-11 05:29:55 +01:00
Elias Batek af25bbbed4 Add unittest to documentation 2025-02-11 05:13:10 +01:00
Elias Batek 7e03da94e8 Add `isSliceOf` to `arsd.core` 2025-02-11 05:12:24 +01:00
Elias Batek 533290373e Fix `parseIniAA` 2025-02-11 04:02:35 +01:00
Elias Batek 89d438982d Implement `Dialect.concatSubstrings` 2025-02-11 03:41:08 +01:00
Elias Batek 3caf37fa14 Add further documentation to and adjust `arsd.ini` 2025-02-10 03:37:42 +01:00
Adam D. Ruppe bdb7372488 make it work again 2025-02-08 16:20:30 -05:00
Adam D. Ruppe eb9abb180e notes about upcoming 12.0 changes 2025-02-08 15:48:46 -05:00
Adam D. Ruppe a3728bdc37 lots of good new stuff 2025-02-08 15:48:25 -05:00
Adam D. Ruppe 6d4683d4ce support for minigui 2025-02-08 15:47:50 -05:00
Adam D. Ruppe 16f17911f6 irrelevant 2025-02-08 15:47:01 -05:00
Adam D. Ruppe 1c13f6fa53 stdout is techncially right check hre 2025-02-08 15:46:41 -05:00
Adam D. Ruppe ea09f6530a just for with expressions 2025-02-08 15:46:18 -05:00
Adam D. Ruppe 34481024a6 64 bit compat 2025-02-08 15:45:51 -05:00
Adam D. Ruppe 01cc666976 safer by default annotations 2025-02-08 15:45:30 -05:00
Adam D. Ruppe 08a584f7f0 ancient 2025-02-08 15:45:11 -05:00
Adam D. Ruppe e592a3a0ac update upstream and 64 bit 2025-02-08 15:44:49 -05:00
Elias Batek f8984fc4b8 Remove unimplemented `IniDialect` option `arrays` 2025-02-08 04:07:31 +01:00
Elias Batek f821ebdc08 Add support for single-quoted strings to `arsd.ini` 2025-02-08 03:32:17 +01:00
Elias Batek c0aed7220a Add `IniFilteredParser` 2025-02-08 03:32:02 +01:00
Elias Batek 2c61ff8ab8 Remove usage of shortened alias from example 2025-02-08 03:14:16 +01:00
Elias Batek d93dd0d167 Add further test cases to `arsd.ini` 2025-02-08 02:36:29 +01:00
Elias Batek 807cc847ba Extend testsuite of `arsd.ini`
Also adds two convenience functions.
2025-02-08 02:17:44 +01:00
Elias Batek 33595b7f87 Fix minor issues 2025-02-08 02:17:24 +01:00
Elias Batek 5d3a57ea1a Add further doc comments to `arsd.ini` 2025-02-07 05:57:35 +01:00
Elias Batek 7d13f7cf22 Add function to parse INI into an AA 2025-02-07 05:56:56 +01:00
Elias Batek c5406b1634 Fix bugs and add further unittests to `arsd.ini` 2025-02-07 05:27:04 +01:00
Elias Batek 2e12f1a8f5 Add convenient INI DOM parser 2025-02-07 05:12:06 +01:00
Elias Batek fcc46ff41b Add `arsd.ini` module 2025-02-07 03:43:52 +01:00
Adam D. Ruppe db37db819c make minimal processing work again for my detachable terminal emulator 2025-02-02 21:20:55 -05:00
Adam D. Ruppe 852d932413
Merge pull request #477 from analogjupiter/pixmappaint
PixmapPaint February ’25 update
2025-02-02 21:20:33 -05:00
Elias Batek f815c0b336 Practise what you preach 2025-02-02 02:21:40 +01:00
Elias Batek f2c15f0e31 Remove janky faux-linear scaler 2025-02-02 01:56:44 +01:00
Elias Batek 122e60da83 Add commented out outline of `SoftwarePixmapRenderer` 2025-02-02 01:53:05 +01:00
Elias Batek 4d74f70ddd Fix downscaler 2025-02-02 01:51:53 +01:00
Elias Batek 4ca96e723b Remove superfluous `SamplingMode` templating 2025-02-02 01:40:17 +01:00
Elias Batek c65c8d462e Fix downscaler 2025-02-02 01:17:27 +01:00
Elias Batek 2005248514 Refactor scaling code 2025-02-01 22:52:02 +01:00
Elias Batek c3beff155c Refactor component foreach loop of bilinear scaler 2025-02-01 07:30:21 +01:00
Elias Batek 2804f426c4 Refactor component/channel loop of image scaler 2025-02-01 05:59:56 +01:00
Elias Batek 539480a2fa Remove unnecessary array 2025-02-01 05:48:58 +01:00
Elias Batek 16994b51f6 Move Y weight calculation 2025-02-01 05:19:08 +01:00
Elias Batek 0108d467ad Improve readability 2025-02-01 05:16:50 +01:00
Elias Batek d9e1e0e84e Refactor y foreach loop of bilinear scaler 2025-02-01 05:13:17 +01:00
Elias Batek 0b288d385f Fix upscaler 2025-02-01 04:37:50 +01:00
Elias Batek c9790d0c19 Fix a few things 2025-01-30 01:26:46 +01:00
Elias Batek 442c616bae Document function `scaleTo` 2025-01-28 02:42:37 +01:00
Elias Batek 31308e0777 Templatize bilinear up/down scaler 2025-01-28 02:29:45 +01:00
Elias Batek 7f91abfc0a Add further unittest for `UDecimal` 2025-01-28 02:13:05 +01:00
Elias Batek 425fb918db Fix incomplete sentence 2025-01-28 02:04:03 +01:00
Elias Batek 6dc177619d Improve `UDecimal` 2025-01-28 02:03:38 +01:00
Elias Batek 9899b48f16 Fix nearest neighbor algorithm 2025-01-28 02:02:09 +01:00
Elias Batek 68a94f03c3 Finish downscaler implementation 2025-01-28 02:01:31 +01:00
Elias Batek b031783e84 Spell "neighbor" without a 'u' 2025-01-25 04:10:09 +01:00
Elias Batek 247cee88d0 Make pixel mapping examples render in visually distinguishable blocks 2025-01-25 04:09:03 +01:00
Elias Batek bb6ad459eb Add improper downscaler implementation (WIP)
To be replaced…
2025-01-14 00:40:22 +01:00
Elias Batek cce1a924ae Add additional operator overloads to `UDecimal` 2025-01-13 03:16:07 +01:00
Elias Batek 6eb6e88594 Revert "Implement `opBinaryRight(string op : "/")` of `UDecimal`"
This reverts commit 266ae6f7dc.

Implementation was impromper and an attempt to fix it outlined
why it was't implemented in the first place.
2025-01-12 23:03:08 +01:00
Elias Batek 4f3dca5a32 Refactor, update and fix PixmapPaint 2025-01-12 22:07:27 +01:00
Elias Batek ebd1c62d69 Rename param `method` of type `ScalingFilter` to `filter` 2025-01-12 06:56:04 +01:00
Elias Batek f1d41403d1 Rename scaling filter `linear` to `bilinear` 2025-01-12 06:53:04 +01:00
Elias Batek bc196985b5 Implement bilinear image upscaling in PixmapPaint 2025-01-12 06:45:46 +01:00
Elias Batek 266ae6f7dc Implement `opBinaryRight(string op : "/")` of `UDecimal` 2025-01-12 02:32:25 +01:00
Elias Batek 78ed1bb287 Rename `UInt32p64` to `UDecimal` 2025-01-12 02:31:19 +01:00
Elias Batek a024404330 Add floor and ceiling rounding functions to `UInt32p64` 2025-01-12 02:15:29 +01:00
Elias Batek 9c5a341bce Move `ScalingFilter` to PixmapPaint 2025-01-12 02:00:52 +01:00
Elias Batek 3c40abb151 Implement "nearest neighbour" scaling in PixmapPaint 2025-01-12 01:14:50 +01:00
Elias Batek 9f280bfbb6 Increase unittest coverage of `UInt32p64` 2025-01-10 04:19:50 +01:00
Elias Batek 1ddb1e1e0e Add UInt32p64 type 2025-01-10 04:14:37 +01:00
Adam D. Ruppe 52c8b346bb fix recursive crash for now 2025-01-03 20:14:22 -05:00
Adam D. Ruppe 953690139b more little tab complete details in the custom file picker 2025-01-03 09:08:40 -05:00
Adam D. Ruppe 2fe1b06e4f huge improvements to ListWidget and FilePicker on custom widgets. ScrollableWidget now set for deprecation pending removal 2025-01-02 20:06:18 -05:00
Adam D. Ruppe be8453fc92 tons of menu and combo box fixes: #469 #468 #470 #471 2024-12-30 10:36:08 -05:00
Adam D. Ruppe 7d977499ba static array support 2024-12-30 10:31:23 -05:00
Adam D. Ruppe f23350a61a minor final trying for speed since they often called 2024-12-30 10:30:55 -05:00
Adam D. Ruppe 23cf22d0e3 more reliable connect loop 2024-12-30 10:30:23 -05:00
Adam D. Ruppe 5618c1e47a oops didnt mean to nuke that file 2024-12-30 10:30:04 -05:00
Adam D. Ruppe b0811314c4 fix memory leak 2024-12-30 10:29:52 -05:00
Adam D. Ruppe 14ee0e0b7e try to tame more of cef badness 2024-11-23 15:22:44 -05:00
Adam D. Ruppe 63b404ca24 doc fix 2024-11-23 15:22:44 -05:00
Adam D. Ruppe 3325e2a75e allow running the scgi and http from the default build 2024-11-23 15:22:44 -05:00
Adam D. Ruppe fd1a316179 overloads to help get mime types right 2024-11-23 15:22:44 -05:00
Adam D. Ruppe 418e1005a8 more file path 2024-11-23 15:22:44 -05:00
Adam D. Ruppe b0557bba5f
Merge pull request #464 from analogjupiter/pixmappaint
PixmapPaint October ’24 update
2024-11-23 15:22:28 -05:00
Adam D. Ruppe a447a7dc9c make xz thing work on multi block files 2024-10-25 12:17:57 -04:00
Adam D. Ruppe 7e6b3c3cbe omg mac 2024-10-24 18:44:46 -04:00
Adam D. Ruppe 7901578797 some emscripten basic support 2024-10-24 18:04:45 -04:00
Adam D. Ruppe dd2815bd2b better error messages for http low level errors 2024-10-24 18:03:38 -04:00
Adam D. Ruppe f74e3bfd4d use new parented dialog for file open - much better window management experience 2024-10-24 18:02:59 -04:00
Adam D. Ruppe 3e0d5b7acd context menu start 2024-10-24 18:02:23 -04:00
Adam D. Ruppe ae07910adb support all elements 2024-10-24 18:01:27 -04:00
Adam D. Ruppe 9a989762e6 omg wrong reading 2024-10-23 08:50:33 -04:00
Elias Batek 4979085a4c Get rid of bold text in the first paragraph of function doc comments 2024-10-13 19:21:11 +02:00
Elias Batek 647c19997d Improve Pixmap.clone() 2024-10-13 19:18:39 +02:00
Elias Batek 195d1b228c Improve docs of PixmapBlueprint 2024-10-13 19:16:49 +02:00
Elias Batek b9098844b1 Refactor rgba(ubyte,ubyte,ubyte,float) 2024-10-13 19:15:03 +02:00
Elias Batek c4485e3f88 Allow "…CalcDims" functions to be less consistent
Not all consistency is good.
2024-10-13 19:04:25 +02:00
Elias Batek a23b056822 Blank lines are fun and good for you 2024-10-13 19:00:26 +02:00
Elias Batek c23f7116c7 Rename "…To" image manipulation functions to "…Into" 2024-10-13 18:55:43 +02:00
Elias Batek ac7f4c9889 Refactor opacity() to decreaseOpacity() & friends 2024-10-13 18:47:06 +02:00
Elias Batek c21d14664c Refactor invert() to match other image manip functions 2024-10-13 17:52:57 +02:00
Elias Batek 75f77176d7 Improve PixmapPaint docs 2024-10-12 23:19:00 +02:00
Elias Batek ebdfcaf799 Add further documentation 2024-10-12 23:04:46 +02:00
Elias Batek dea6de12e0 Improve and add image manipulation functions 2024-10-12 23:04:24 +02:00
Elias Batek 31a247c4c9 Implement PixmapBlueprints 2024-10-12 23:00:39 +02:00
Adam D. Ruppe 80fc783116 more clipboard hax on x 2024-10-10 17:12:25 -04:00
Elias Batek 7d39086857 Rename scanSubPixmap() to scanArea() 2024-10-08 00:22:35 +02:00
Elias Batek 0bdea48b9b Fix bidirectional features of SubPixmapScanner 2024-10-08 00:21:53 +02:00
Elias Batek 8aaaa2a3c2 Implement 180° rotation function 2024-10-07 04:50:28 +02:00
Elias Batek cf2e084a70 Fix flipVertically() 2024-10-07 04:17:22 +02:00
Elias Batek 582e47c13b Implement vertical flipping 2024-10-07 04:06:31 +02:00
Elias Batek a2f437d4ad Make PixmapScanner types bidirectional ranges 2024-10-07 03:43:44 +02:00
Elias Batek db6b6d1f74 Slightly improve docs and usability 2024-10-07 03:24:22 +02:00
Elias Batek 1fbdcab948 Implement horizontal flipping 2024-10-07 03:23:36 +02:00
Elias Batek 65ba2793cb Move that back
Why did I move this in the first place?
2024-10-07 02:09:33 +02:00
Elias Batek 9fdaf00197 Fix the coordinate system diagram
To quote one of my IN(formation)SY(stems) teachers,
“Sorry, this is just a copy-paste mistake.”
2024-10-07 01:57:46 +02:00
Elias Batek e5841da630 Add further general documentation to PixmapPaint 2024-10-07 01:49:00 +02:00
Elias Batek fc346c3ec2 Fix cropInPlace() 2024-10-07 01:48:04 +02:00
Elias Batek 6978d1f2dd Implement clockwise rotation 2024-10-07 00:17:36 +02:00
Elias Batek d60426e833 Add Pixmap copy function 2024-10-06 21:52:39 +02:00
Elias Batek 8bf54227ae Add documentation for the crop functions 2024-10-06 20:31:01 +02:00
Adam D. Ruppe ae50e5dc68
Merge pull request #463 from analogjupiter/presenter-docs-20241006
Small documentation improvement for PixmapPresenter
2024-10-06 07:16:02 -04:00
Elias Batek 334ccb42ce Mention `Scaling.intHybrid` in code example 2024-10-06 04:09:32 +02:00
Elias Batek 359edb26ff Remove unnecessary “the” from comment 2024-10-06 04:09:25 +02:00
Elias Batek eade7a3754 Fix and improve documentation of PixmapPaint 2024-10-06 03:55:04 +02:00
Elias Batek 1d2f907d3c Implement Pixmap cropping 2024-10-06 03:54:47 +02:00
Elias Batek 33cd84552b Fix refucktoring regression 2024-10-06 01:43:20 +02:00
Elias Batek 9418cbaa24 Document the coordinate system used by PixmapPaint 2024-10-06 01:35:08 +02:00
Elias Batek 0bdcc43a57 Fix SubPixmap 2024-10-06 01:34:51 +02:00
Elias Batek a391b8dad9 Remove leftover debug{writeln();} 2024-10-06 01:34:18 +02:00
Elias Batek 38301f1507 Document SubPixmap.xferTo()
and improve docs slightly.
2024-10-06 00:19:32 +02:00
Elias Batek 1d0822c89a Rename SubPixmap.isSound() to SubPixmap.isValid() 2024-10-06 00:19:19 +02:00
Elias Batek d83ae00982 Make the pure round()ing function private 2024-10-06 00:00:42 +02:00
Elias Batek 011abf026e Add SubPixmap support to PixmapPaint 2024-10-05 23:59:55 +02:00
Adam D. Ruppe eaf60b4101 note about buffer swapping 2024-10-05 10:45:17 -04:00
Adam D. Ruppe 4947ea9efd
Merge pull request #462 from analogjupiter/sdpy-apitrace
Add version(apitrace) to simpledisplay
2024-10-05 08:28:16 -04:00
Adam D. Ruppe 9862220365
Merge pull request #460 from analogjupiter/pmp-clearfix
Fix PixmapPresenter not glClear()'ing as expected
2024-10-05 07:47:58 -04:00
Elias Batek 3e661c407f Fix PixmapPresenter not glClear()'ing as expected
No more “smart” clearing shenanigans – those didn’t work properly
anyway.
2024-10-05 04:52:14 +02:00
Elias Batek 152da60927 Add version(apitrace) to simpledisplay 2024-10-05 04:38:08 +02:00
Adam D. Ruppe 49f28a9e0f always clear selection cache on event since it is static across windows anyway 2024-10-04 22:12:56 -04:00
Adam D. Ruppe 0fce8ff5e5
Merge pull request #461 from analogjupiter/despace
Fix tabs/space mixing in sdpy
2024-10-04 22:12:34 -04:00
Elias Batek 379019c72e Fix tabs/space mixing in sdpy 2024-10-05 03:38:05 +02:00
Adam D. Ruppe fc4d833235 minigui - make dialog/message boxes less bad
by setting them correctly as transient to their originator window, the
window manager can place them better and set the focus as expected
automatically
2024-09-29 21:14:40 -04:00
Adam D. Ruppe 115be86c63 make show/hide work better including with native widgets when you have hidden grandchildren 2024-09-28 21:26:13 -04:00
Adam D. Ruppe 1aff5c0293 trying to fix minigui's selection hacks and shrinkiness 2024-09-28 17:08:31 -04:00
Adam D. Ruppe 161d733196 some css fixes 2024-09-24 09:18:33 -04:00
Adam D. Ruppe 49dee2642b a bit of cleanup 2024-09-23 21:34:24 -04:00
Adam D. Ruppe 21eec2bc9b use new object from dom.d, breaking change - change your string[string] to AttributesHolder too 2024-09-23 21:33:17 -04:00
Adam D. Ruppe 2f457a1dbe hacks to make it build on Windows 2024-09-23 21:32:32 -04:00
Adam D. Ruppe 6c8d967086 safer by default 2024-09-23 21:32:10 -04:00
Adam D. Ruppe 697214132d port of commommark for my use 2024-09-23 16:10:41 -04:00
Adam D. Ruppe a9a9d75988 mac safer by default 2024-09-22 14:32:20 -04:00
Adam D. Ruppe c13a8cd2e5 more safer by default warning silencing 2024-09-15 09:21:01 -04:00
Adam D. Ruppe d67fae5b1f bold fonts on linux were so wrong lol 2024-09-15 08:38:37 -04:00
Adam D. Ruppe 156e02bee9 more html parsing woes 2024-09-15 08:38:21 -04:00
Adam D. Ruppe d246f4d744 ExternalProcess overhaul to be better pipe citizens 2024-09-13 22:05:01 -04:00
Adam D. Ruppe d5a59f8a3b screenpainter emulator for my block puzzle DO NOT DEPEND ON YEY 2024-09-13 22:04:39 -04:00
Adam D. Ruppe 86ab28abf0 better reconnecting 2024-09-13 22:04:08 -04:00
Adam D. Ruppe 9f9cf2e290 useful month names 2024-09-13 22:03:53 -04:00
Adam D. Ruppe fadcce6082 dox 2024-09-13 22:03:43 -04:00
Adam D. Ruppe 25e46e9d2d opengl3 limited font wip 2024-09-13 22:03:22 -04:00
Adam D. Ruppe 368d1d8c59 some matrix math 2024-09-13 22:02:59 -04:00
Adam D. Ruppe 6e4e2966ca some browser jam stuff 2024-09-13 22:02:07 -04:00
Adam D. Ruppe 21c8eb6cbd handle some other non-nestable html tags too 2024-09-13 15:18:22 -04:00
Adam D. Ruppe 20e675ee99 omg to and cc are not supposed to be the same 2024-09-13 07:51:55 -04:00
Adam D. Ruppe b1c1d86fb8
Merge pull request #458 from analogjupiter/suppress-auto-opengl-viewport
Suppress sdpy's auto-OpenGL-viewport in PixmapPresenter
2024-09-10 22:01:18 -04:00
Elias Batek 9c4a452bb2 Suppress sdpy's auto-OpenGL-viewport in PixmapPresenter 2024-09-11 03:58:21 +02:00
Adam D. Ruppe 5dd6f2bc54
Merge pull request #457 from analogjupiter/timer-conflict
Timer conflict
2024-09-10 21:21:55 -04:00
Elias Batek 2e05986ade Update history of arsd.core.Timer 2024-09-11 03:18:54 +02:00
Elias Batek 39e87df602 Fix "Timer" symbol clash 2024-09-11 03:08:13 +02:00
Adam D. Ruppe 4f2b94e790 omg use after free bug was there 2024-09-09 21:58:13 -04:00
Adam D. Ruppe 701fdf5e25 %F not supported on Windows lol 2024-09-09 21:12:39 -04:00
Adam D. Ruppe ae07d697a3 make compile times great again 2024-09-09 20:51:28 -04:00
Adam D. Ruppe 6d30f2fba9
Merge pull request #456 from analogjupiter/corner-style
Implement "corner style" property for sdpy windows on Windows 11+
2024-09-09 20:47:03 -04:00
Elias Batek a9a596dc20 Support slightly rounded window corners too 2024-09-10 02:29:59 +02:00
Adam D. Ruppe 547ded1eeb
Merge pull request #455 from analogjupiter/json-syntax
Fix JSON syntax
2024-09-09 20:22:15 -04:00
Elias Batek 376fde2e8a Fix JSON syntax 2024-09-10 02:19:43 +02:00
Elias Batek b6b12995f9 Add corner-style preference setting to PixmapPresenter 2024-09-10 02:17:22 +02:00
Elias Batek bbc7aec494 Implement corner-style for sdpy windows on Windows 11+ 2024-09-10 02:15:02 +02:00
Adam D. Ruppe 2e8d9cdeb9
Merge pull request #453 from dkorpel/patch-2
core.d: Fix wrong `return` annotation on UserProvidedBuffer.slice
2024-09-03 07:36:59 -04:00
Dennis 31ab53959e
Fix wrong `return` annotation
There's a compile error when using `-preview=dip1000`:

```D
	static SocketAddress[] localhost(ushort port, return UserProvidedBuffer!SocketAddress buffer = null) {
		buffer.append(ip6("::1", port));
		buffer.append(ip4("127.0.0.1", port));
		return buffer.slice;
	}
```

```
C:\Users\Dennis\AppData\Local\dub\packages\arsd-official\11.4.2\arsd-official\core.d(3066,10): Error: returning `buffer.slice()` escapes a reference to parameter `buffer`
C:\Users\Dennis\AppData\Local\dub\packages\arsd-official\11.4.2\arsd-official\core.d(3181,10): Error: returning `buffer.slice()` escapes a reference to parameter `buffer`
```

The `UserProvidedBuffer.slice` method is annotated `return (ref)` but it should be `return scope`, or since it's a template, just inferred.
2024-09-03 11:48:38 +02:00
Adam D. Ruppe 5c26eeb447 trying to track down a random hang and as quick as it came it was gone agian but meh 2024-08-29 20:33:12 -04:00
Adam D. Ruppe 9881f555a5 some more screenpainter compat in arsd.game 2024-08-27 10:19:41 -04:00
Adam D. Ruppe 3cc5028486 new dmd compat 2024-08-27 10:19:32 -04:00
Adam D. Ruppe cdeca0a686 preserve order of attributes with new wrapper - has breaking changes 2024-08-27 10:19:12 -04:00
Adam D. Ruppe a49f64893c a few adrdox fixups 2024-08-20 16:06:01 -04:00
Adam D. Ruppe d646bc17ca
Merge pull request #451 from analogjupiter/pixmappaint
Document Pixmap Paint module
2024-08-18 12:24:58 -04:00
Adam D. Ruppe cb76629376
Merge pull request #450 from analogjupiter/pixmap-recorder
Pixmap Recorder revision Ⅰ
2024-08-18 12:24:49 -04:00
Elias Batek 8a68748bd6 Adjust placement of @safe for better readability 2024-08-18 03:39:20 +02:00
Elias Batek 3b33ac7f30 Document arsd.pixmappaint module 2024-08-18 03:24:26 +02:00
Elias Batek 997a7c8fd5 Fix spelling and grammar 2024-08-18 02:59:28 +02:00
Elias Batek 8efa5c7720 Overhaul Pixmap Recorder 2024-08-18 02:40:18 +02:00
Elias Batek f432e7d744 Improve documentation of Pixmap Recorder 2024-08-18 02:11:34 +02:00
Elias Batek 1d39d3b61e Remove unused stdout-getter from Pixmap Recorder
Wasn't even properly implemented anyway
and would have returned `stderr` instead.
2024-08-18 01:19:52 +02:00
Elias Batek 2a12df337d Mark PixmapRecorder as Output Range
Because why not? :P
2024-08-18 01:17:49 +02:00
Elias Batek 2f32267898 Fix bogus return statement in PixmapRecorder.close() 2024-08-18 01:16:48 +02:00
Adam D. Ruppe b6e54b8b85
Merge pull request #449 from analogjupiter/pixmap-recorder
Add Pixmap Recorder module
2024-08-17 07:36:15 -04:00
Elias Batek d209923433 Allow PixmapRecorder.close() to be called on inactive recorders 2024-08-17 06:10:16 +02:00
Elias Batek a49e7c16e5 Add Pixmap Recorder to the changelog 2024-08-17 05:39:22 +02:00
Elias Batek f1b69132af Improve description of Pixmap Recorder 2024-08-17 05:35:16 +02:00
Elias Batek aba6a49f74 Add Pixmap Recorder module 2024-08-17 05:32:32 +02:00
39 changed files with 25986 additions and 1962 deletions

View File

@ -22,13 +22,27 @@ This only lists changes that broke things and got a major version bump. I didn't
Please note that I DO consider changes to build process to be a breaking change, but I do NOT consider symbol additions, changes to undocumented members, or the occasional non-fatal deprecation to be breaking changes. Undocumented members may be changed at any time, whereas additions and/or deprecations will be a minor version change.
## 12.0
## 13.0
Future release, likely May 2024 or later.
Future release, likely May 2026 or later.
Nothing is planned for it at this time.
arsd.pixmappresenter and arsd.pixmappaint were added.
## 12.0
Released: Planned for some time between January and May 2025
minigui's `defaultEventHandler_*` functions take more specific objects. So if you see errors like:
```
Error: function `void arsd.minigui.EditableTextWidget.defaultEventHandler_focusin(Event foe)` does not override any function, did you mean to override `void arsd.minigui.Widget.defaultEventHandler_focusin(arsd.minigui.FocusInEvent event)`?
```
Go to the file+line number from the error message and change `Event` to `FocusInEvent` (or whatever one it tells you in the "did you mean" part of the error) and recompile. No other changes should be necessary, however if you constructed your own `Event` object and dispatched it with the loosely typed `"focus"`, etc., strings, it may not trigger the default handlers anymore. To fix this, change any `new Event` to use the appropriate subclass, when available, like old `new Event("focus", widget);` changes to `new FocusEvent(widget)`. This only applies to ones that trigger default handlers present in `Widget` base class; your custom events still work the same way.
arsd.pixmappresenter, arsd.pixmappaint and arsd.pixmaprecorder were added.
arsd.ini was added.
## 11.0

169
archive.d
View File

@ -22,6 +22,8 @@
A number of improvements were made with the help of Steven Schveighoffer on March 22, 2023.
`arsd.archive` was changed to require [arsd.core] on March 23, 2023 (dub v11.0). Previously, it was a standalone module. It uses arsd.core's exception helpers only at this time and you could turn them back into plain (though uninformative) D base `Exception` instances to remove the dependency if you wanted to keep the file independent.
The [ArzArchive] class had a memory leak prior to November 2, 2024. It now uses the GC instead.
+/
module arsd.archive;
@ -216,23 +218,23 @@ unittest {
// Advances data up to the end of the vla
ulong readVla(ref const(ubyte)[] data) {
ulong n;
n = data[0] & 0x7f;
if(!(data[0] & 0x80))
data = data[1 .. $];
ulong n = 0;
int i = 0;
while(data[0] & 0x80) {
i++;
while (data[0] & 0x80) {
ubyte b = data[0];
data = data[1 .. $];
ubyte b = data[0];
assert(b != 0);
if(b == 0) return 0;
n |= cast(ulong) (b & 0x7F) << (i * 7);
n |= cast(ulong)(b & 0x7F) << (i * 7);
i++;
}
ubyte b = data[0];
data = data[1 .. $];
n |= cast(ulong)(b & 0x7F) << (i * 7);
return n;
}
@ -246,12 +248,14 @@ ulong readVla(ref const(ubyte)[] data) {
chunkBuffer = an optional parameter providing memory that will be used to buffer uncompressed data chunks. If you pass `null`, it will allocate one for you. Any data in the buffer will be immediately overwritten.
inputBuffer = an optional parameter providing memory that will hold compressed input data. If you pass `null`, it will allocate one for you. You should NOT populate this buffer with any data; it will be immediately overwritten upon calling this function. The `inputBuffer` must be at least 32 bytes in size.
inputBuffer = an optional parameter providing memory that will hold compressed input data. If you pass `null`, it will allocate one for you. You should NOT populate this buffer with any data; it will be immediately overwritten upon calling this function. The `inputBuffer` must be at least 64 bytes in size.
allowPartialChunks = can be set to true if you want `chunkReceiver` to be called as soon as possible, even if it is only partially full before the end of the input stream. The default is to fill the input buffer for every call to `chunkReceiver` except the last which has remainder data from the input stream.
History:
Added March 24, 2023 (dub v11.0)
On October 25, 2024, the implementation got a major fix - it can read multiple blocks off the xz file now, were as before it would stop at the first one. This changed the requirement of the input buffer minimum size from 32 to 64 bytes (but it is always better to go more, I recommend 32 KB).
+/
version(WithLzmaDecoder)
void decompressLzma(scope void delegate(in ubyte[] chunk) chunkReceiver, scope ubyte[] delegate(ubyte[] buffer) bufferFiller, ubyte[] chunkBuffer = null, ubyte[] inputBuffer = null, bool allowPartialChunks = false) @trusted {
@ -260,6 +264,10 @@ void decompressLzma(scope void delegate(in ubyte[] chunk) chunkReceiver, scope u
if(inputBuffer is null)
inputBuffer = new ubyte[](1024 * 32);
assert(inputBuffer.length >= 64);
bool isStartOfFile = true;
const(ubyte)[] compressedData = bufferFiller(inputBuffer[]);
XzDecoder decoder = XzDecoder(compressedData);
@ -439,11 +447,51 @@ struct XzDecoder {
//uint crc32 = initialData[0 .. 4]; // FIXME just cast it. this is the crc of the flags.
initialData = initialData[4 .. $];
state = State.readingHeader;
readBlockHeader(initialData);
}
private enum State {
readingHeader,
readingData,
readingFooter,
}
private State state;
// returns true if it successfully read it, false if it needs more data
private bool readBlockHeader(const(ubyte)[] initialData) {
// now we are into an xz block...
if(initialData.length == 0) {
unprocessed = initialData;
needsMoreData_ = true;
finished_ = false;
return false;
}
if(initialData[0] == 0) {
// this is actually an index and a footer...
// we could process it but this also really marks us being done!
// FIXME: should actually pull the data out and finish it off
// see Index records etc at https://tukaani.org/xz/xz-file-format.txt
unprocessed = null;
finished_ = true;
needsMoreData_ = false;
return true;
}
int blockHeaderSize = (initialData[0] + 1) * 4;
auto first = initialData.ptr;
if(blockHeaderSize > initialData.length) {
unprocessed = initialData;
needsMoreData_ = true;
finished_ = false;
return false;
}
auto srcPostHeader = initialData[blockHeaderSize .. $];
initialData = initialData[1 .. $];
@ -453,12 +501,18 @@ struct XzDecoder {
if(blockFlags & 0x40) {
compressedSize = readVla(initialData);
} else {
compressedSize = 0;
}
if(blockFlags & 0x80) {
uncompressedSize = readVla(initialData);
} else {
uncompressedSize = 0;
}
//import std.stdio; writeln(compressedSize , " compressed, expands to ", uncompressedSize);
auto filterCount = (blockFlags & 0b11) + 1;
ubyte props;
@ -467,6 +521,7 @@ struct XzDecoder {
auto fid = readVla(initialData);
auto sz = readVla(initialData);
// import std.stdio; writefln("%02x %d", fid, sz);
assert(fid == 0x21);
assert(sz == 1);
@ -474,13 +529,23 @@ struct XzDecoder {
initialData = initialData[1 .. $];
}
//writeln(src.ptr);
//writeln(srcPostHeader.ptr);
// writeln(initialData.ptr);
// writeln(srcPostHeader.ptr);
// there should be some padding to a multiple of 4...
// three bytes of zeroes given the assumptions here
initialData = initialData[3 .. $];
assert(blockHeaderSize >= 4);
long expectedRemainder = cast(long) blockHeaderSize - 4;
expectedRemainder -= initialData.ptr - first;
assert(expectedRemainder >= 0);
while(expectedRemainder) {
expectedRemainder--;
if(initialData[0] != 0)
throw new Exception("non-zero where padding byte expected in xz file");
initialData = initialData[1 .. $];
}
// and then a header crc
@ -505,6 +570,31 @@ struct XzDecoder {
Lzma2Dec_Init(&lzmaDecoder);
unprocessed = initialData;
state = State.readingData;
return true;
}
private bool readBlockFooter(const(ubyte)[] data) {
// skip block padding
while(data.length && data[0] == 0) {
data = data[1 .. $];
}
if(data.length < checkSize) {
unprocessed = data;
finished_ = false;
needsMoreData_ = true;
return false;
}
// skip the check
data = data[checkSize .. $];
state = State.readingHeader;
return readBlockHeader(data);
//return true;
}
~this() {
@ -595,7 +685,12 @@ struct XzDecoder {
+/
ubyte[] processData(ubyte[] dest, const ubyte[] src) {
ubyte[] processData(ubyte[] dest, const(ubyte)[] src) {
if(state == State.readingHeader) {
if(!readBlockHeader(src))
return dest[0 .. 0];
src = unprocessed;
}
size_t destLen = dest.length;
size_t srcLen = src.length;
@ -628,9 +723,11 @@ struct XzDecoder {
finished_ = false;
needsMoreData_ = true;
} else if(status == LZMA_STATUS_FINISHED_WITH_MARK || status == LZMA_STATUS_MAYBE_FINISHED_WITHOUT_MARK) {
unprocessed = null;
finished_ = true;
needsMoreData_ = false;
// this is the end of a block, but not necessarily the end of the file
state = State.readingFooter;
// the readBlockFooter function updates state, unprocessed, finished, and needs more data
readBlockFooter(src[srcLen .. $]);
} else if(status == LZMA_STATUS_NOT_FINISHED) {
unprocessed = src[srcLen .. $];
finished_ = false;
@ -818,13 +915,13 @@ private:
assert(nfo.rc);
if (--nfo.rc == 0) {
import core.memory : GC;
import core.stdc.stdlib : free;
// import core.stdc.stdlib : free;
if (nfo.afl !is null) fclose(nfo.afl);
nfo.chunks.destroy;
nfo.files.destroy;
nfo.afl = null;
GC.removeRange(cast(void*)nfo/*, Nfo.sizeof*/);
free(nfo);
xfree(nfo);
debug(arcz_rc) { import core.stdc.stdio : printf; printf("Nfo %p freed\n", nfo); }
}
}
@ -866,11 +963,13 @@ private:
}
static T* xalloc(T, bool clear=true) (uint mem) if (T.sizeof > 0) {
import core.memory;
import core.exception : onOutOfMemoryError;
assert(mem != 0);
static if (clear) {
import core.stdc.stdlib : calloc;
auto res = calloc(mem, T.sizeof);
// import core.stdc.stdlib : calloc;
// auto res = calloc(mem, T.sizeof);
auto res = GC.calloc(mem * T.sizeof, GC.BlkAttr.NO_SCAN);
if (res is null) onOutOfMemoryError();
static if (is(T == struct)) {
import core.stdc.string : memcpy;
@ -878,10 +977,12 @@ private:
foreach (immutable idx; 0..mem) memcpy(res+idx, &i, T.sizeof);
}
debug(arcz_alloc) { import core.stdc.stdio : printf; printf("allocated %u bytes at %p\n", cast(uint)(mem*T.sizeof), res); }
debug(arcz_alloc) { try { throw new Exception("mem trace c"); } catch(Exception e) { import std.stdio; writeln(e.toString()); } }
return cast(T*)res;
} else {
import core.stdc.stdlib : malloc;
auto res = malloc(mem*T.sizeof);
//import core.stdc.stdlib : malloc;
//auto res = malloc(mem*T.sizeof);
auto res = GC.malloc(mem*T.sizeof, GC.BlkAttr.NO_SCAN);
if (res is null) onOutOfMemoryError();
static if (is(T == struct)) {
import core.stdc.string : memcpy;
@ -889,16 +990,26 @@ private:
foreach (immutable idx; 0..mem) memcpy(res+idx, &i, T.sizeof);
}
debug(arcz_alloc) { import core.stdc.stdio : printf; printf("allocated %u bytes at %p\n", cast(uint)(mem*T.sizeof), res); }
debug(arcz_alloc) { try { throw new Exception("mem trace"); } catch(Exception e) { import std.stdio; writeln(e.toString()); } }
return cast(T*)res;
}
}
static void xfree(T) (T* ptr) {
// just let the GC do it
if(ptr !is null) {
import core.memory;
GC.free(ptr);
}
/+
if (ptr !is null) {
import core.stdc.stdlib : free;
debug(arcz_alloc) { import core.stdc.stdio : printf; printf("freing at %p\n", ptr); }
free(ptr);
}
+/
}
static if (arcz_has_balz) static ubyte balzDictSize (uint blockSize) {
@ -1169,11 +1280,11 @@ private:
auto zl = cast(LowLevelPackedRO*)me;
assert(zl.rc);
if (--zl.rc == 0) {
import core.stdc.stdlib : free;
if (zl.chunkData !is null) free(zl.chunkData);
version(arcz_use_more_memory) if (zl.pkdata !is null) free(zl.pkdata);
//import core.stdc.stdlib : free;
if (zl.chunkData !is null) xfree(zl.chunkData);
version(arcz_use_more_memory) if (zl.pkdata !is null) xfree(zl.pkdata);
Nfo.decRef(zl.nfop);
free(zl);
xfree(zl);
debug(arcz_rc) { import core.stdc.stdio : printf; printf("Zl %p freed\n", zl); }
} else {
//debug(arcz_rc) { import core.stdc.stdio : printf; printf("Zl %p; rc after decRef is %u\n", zl, zl.rc); }

View File

@ -67,7 +67,7 @@ class Audio{
active = false;
return;
}
if(Mix_OpenAudio(22050, AUDIO_S16SYS, 2, 4096/2 /* the /2 is new */) != 0){
if(1) { // if(Mix_OpenAudio(22050, AUDIO_S16SYS, 2, 4096/2 /* the /2 is new */) != 0){
active = false; //throw new Error;
error = true;
audioIsLoaded = false;

View File

@ -273,3 +273,29 @@ struct ICalParser {
}
}
immutable monthNames = [
"",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
];
immutable daysOfWeekNames = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];

68
cgi.d
View File

@ -188,6 +188,12 @@ void main() {
For an embedded HTTP server, run `dmd yourfile.d cgi.d -version=embedded_httpd` and run the generated program. It listens on port 8085 by default. You can change this on the command line with the --port option when running your program.
Command_line_interface:
If using [GenericMain] or [DispatcherMain], an application using arsd.cgi will offer a command line interface out of the box.
See [RequestServer.listenSpec] for more information.
Simulating_requests:
If you are using one of the [GenericMain] or [DispatcherMain] mixins, or main with your own call to [RequestServer.trySimulatedRequest], you can simulate requests from your command-ine shell. Call the program like this:
@ -616,6 +622,8 @@ version(Posix) {
} else {
version(FreeBSD) {
// not implemented on bsds
} else version(OpenBSD) {
// I never implemented the fancy stuff there either
} else {
version=with_breaking_cgi_features;
@ -4167,9 +4175,34 @@ struct RequestServer {
} else
version(stdio_http) {
serveSingleHttpConnectionOnStdio!(fun, CustomCgi, maxContentLength)();
} else {
//version=plain_cgi;
} else
version(plain_cgi) {
handleCgiRequest!(fun, CustomCgi, maxContentLength)();
} else {
if(this.listenSpec.length) {
// FIXME: what about heterogeneous listen specs?
if(this.listenSpec[0].startsWith("scgi:"))
serveScgi!(fun, CustomCgi, maxContentLength)();
else
serveEmbeddedHttp!(fun, CustomCgi, maxContentLength)();
} else {
import std.process;
if("REQUEST_METHOD" in environment) {
// GATEWAY_INTERFACE must be set according to the spec for it to be a cgi request
// REQUEST_METHOD must also be set
handleCgiRequest!(fun, CustomCgi, maxContentLength)();
} else {
import std.stdio;
writeln("To start a local-only http server, use `thisprogram --listen http://localhost:PORT_NUMBER`");
writeln("To start a externally-accessible http server, use `thisprogram --listen http://:PORT_NUMBER`");
writeln("To start a scgi server, use `thisprogram --listen scgi://localhost:PORT_NUMBER`");
writeln("To test a request on the command line, use `thisprogram REQUEST /path arg=value`");
writeln("Or copy this program to your web server's cgi-bin folder to run it that way.");
writeln("If you need FastCGI, recompile this program with -version=fastcgi");
writeln();
writeln("Learn more at https://opendlang.org/library/arsd.cgi.html#Command-line-interface");
}
}
}
}
@ -6896,12 +6929,14 @@ version(cgi_with_websocket) {
return true;
}
if(bfr.sourceClosed)
if(bfr.sourceClosed) {
return false;
}
bfr.popFront(0);
if(bfr.sourceClosed)
if(bfr.sourceClosed) {
return false;
}
goto top;
}
@ -10711,7 +10746,7 @@ html", true, true);
return dl;
} else static if(is(T == bool)) {
return Element.make("span", t ? "true" : "false", "automatic-data-display");
} else static if(is(T == E[], E)) {
} else static if(is(T == E[], E) || is(T == E[N], E, size_t N)) {
static if(is(E : RestObject!Proxy, Proxy)) {
// treat RestObject similar to struct
auto table = cast(Table) Element.make("table");
@ -11987,27 +12022,8 @@ auto serveStaticData(string urlPrefix, immutable(void)[] data, string contentTyp
}
string contentTypeFromFileExtension(string filename) {
if(filename.endsWith(".png"))
return "image/png";
if(filename.endsWith(".apng"))
return "image/apng";
if(filename.endsWith(".svg"))
return "image/svg+xml";
if(filename.endsWith(".jpg"))
return "image/jpeg";
if(filename.endsWith(".html"))
return "text/html";
if(filename.endsWith(".css"))
return "text/css";
if(filename.endsWith(".js"))
return "application/javascript";
if(filename.endsWith(".wasm"))
return "application/wasm";
if(filename.endsWith(".mp3"))
return "audio/mpeg";
if(filename.endsWith(".pdf"))
return "application/pdf";
return null;
import arsd.core;
return FilePath(filename).contentTypeFromFileExtension();
}
/// This serves a directory full of static files, figuring out the content-types from file extensions.

20
color.d
View File

@ -1906,6 +1906,23 @@ struct Point {
Size opCast(T : Size)() inout @nogc {
return Size(x, y);
}
/++
Calculates the point of linear offset in a rectangle.
`Offset = 0` is assumed to be equivalent to `Point(0,0)`.
See_also:
[linearOffset] is the inverse function.
History:
Added October 05, 2024.
+/
static Point fromLinearOffset(int linearOffset, int width) @nogc {
const y = (linearOffset / width);
const x = (linearOffset % width);
return Point(x, y);
}
}
///
@ -1959,6 +1976,9 @@ struct Size {
Returns:
`y * width + x`
See_also:
[Point.fromLinearOffset] is the inverse function.
History:
Added December 19, 2023 (dub v11.4)
+/

4
com.d
View File

@ -1159,7 +1159,7 @@ extern (D) void ObjectDestroyed()
}
char[] oleCharsToString(char[] buffer, OLECHAR* chars) {
char[] oleCharsToString(char[] buffer, OLECHAR* chars) @system {
auto c = cast(wchar*) chars;
auto orig = c;
@ -1470,7 +1470,7 @@ BOOL SetKeyAndValue(LPCSTR pszKey, LPCSTR pszSubkey, LPCSTR pszValue)
return result;
}
void unicode2ansi(char *s)
void unicode2ansi(char *s) @system
{
wchar *w;

1432
core.d

File diff suppressed because it is too large Load Diff

View File

@ -217,6 +217,7 @@ struct DatabaseDatum {
alias toString this;
/// ditto
version(D_OpenD) {} else // opend enables -preview=rvaluerefparam which makes this conflict with the rvalue toString in matching to!T stuff
T opCast(T)() {
import std.conv;
return to!T(this.toString);

View File

@ -22,6 +22,10 @@
+/
module arsd.discord;
// FIXME: it thought it was still alive but showed as not online and idk why. maybe setPulseCallback stopped triggering?
// FIXME: Secure Connect Failed sometimes on trying to reconnect, should prolly just try again after a short period, or ditch the whole thing if reconnectAndResume and try fresh
// FIXME: User-Agent: DiscordBot ($url, $versionNumber)
import arsd.http2;
@ -665,8 +669,14 @@ class DiscordGatewayConnection {
if(heartbeatTimer)
heartbeatTimer.cancel();
if(closeEvent.code == 1006 || closeEvent.code == 1001)
if(closeEvent.code == 1006 || closeEvent.code == 1001) {
reconnectAndResume();
} else {
// otherwise, unless we were asked by the api user to close, let's try reconnecting
// since discord just does discord things.
websocket_ = null;
connect();
}
}
/++
@ -741,7 +751,7 @@ class DiscordGatewayConnection {
websocket.onmessage = &handleWebsocketMessage;
websocket.onclose = &handleWebsocketClose;
this.websocket_.connect();
websocketConnectInLoop();
var resumeData = var.emptyObject;
resumeData.token = this.token;
@ -917,8 +927,7 @@ class DiscordGatewayConnection {
websocket.onmessage = &handleWebsocketMessage;
websocket.onclose = &handleWebsocketClose;
websocket.connect();
websocketConnectInLoop();
var d = var.emptyObject;
d.token = token;
@ -931,6 +940,33 @@ class DiscordGatewayConnection {
sendWebsocketCommand(OpCode.Identify, d);
}
void websocketConnectInLoop() {
// FIXME: if the connect fails we should set a timer and try
// again, but if it fails then, quit. at least if it is not a websocket reply
// cuz it could be discord went down or something.
import core.time;
auto d = 1.seconds;
int count = 0;
try_again:
try {
this.websocket_.connect();
} catch(Exception e) {
import core.thread;
Thread.sleep(d);
d *= 2;
count++;
if(count == 10)
throw e;
goto try_again;
}
}
}
class DiscordRpcConnection {

66
docx.d Normal file
View File

@ -0,0 +1,66 @@
/++
Bare minimum support for reading Microsoft Word files.
History:
Added February 19, 2025
+/
module arsd.docx;
import arsd.core;
import arsd.zip;
import arsd.dom;
import arsd.color;
/++
+/
class DocxFile {
private ZipFile zipFile;
private XmlDocument document;
/++
+/
this(FilePath file) {
this.zipFile = new ZipFile(file);
load();
}
/// ditto
this(immutable(ubyte)[] rawData) {
this.zipFile = new ZipFile(rawData);
load();
}
/++
Converts the document to a plain text string that gives you
the jist of the document that you can view in a plain editor.
Most formatting is stripped out.
+/
string toPlainText() {
string ret;
foreach(paragraph; document.querySelectorAll("w\\:p")) {
if(ret.length)
ret ~= "\n\n";
ret ~= paragraph.innerText;
}
return ret;
}
// FIXME: to RTF, markdown, html, and terminal sequences might also be useful.
private void load() {
loadXml("word/document.xml", (document) {
this.document = document;
});
}
private void loadXml(string filename, scope void delegate(XmlDocument document) handler) {
auto document = new XmlDocument(cast(string) zipFile.getContent(filename));
handler(document);
}
}

670
dom.d
View File

@ -140,6 +140,151 @@ class Document : FileResource, DomParent {
inout(Document) asDocument() inout { return this; }
inout(Element) asElement() inout { return null; }
/++
These three functions, `processTagOpen`, `processTagClose`, and `processNodeWhileParsing`, allow you to process elements as they are parsed and choose to not append them to the dom tree.
`processTagOpen` is called as soon as it reads the tag name and attributes into the passed `Element` structure, in order
of appearance in the file. `processTagClose` is called similarly, when that tag has been closed. In between, all descendant
nodes - including tags as well as text and other nodes - are passed to `processNodeWhileParsing`. Finally, after `processTagClose`,
the node itself is passed to `processNodeWhileParsing` only after its children.
So, given:
```xml
<thing>
<child>
<grandchild></grandchild>
</child>
</thing>
```
It would call:
$(NUMBERED_LIST
* processTagOpen(thing)
* processNodeWhileParsing(thing, whitespace text) // the newlines, spaces, and tabs between the thing tag and child tag
* processTagOpen(child)
* processNodeWhileParsing(child, whitespace text)
* processTagOpen(grandchild)
* processTagClose(grandchild)
* processNodeWhileParsing(child, grandchild)
* processNodeWhileParsing(child, whitespace text) // whitespace after the grandchild
* processTagClose(child)
* processNodeWhileParsing(thing, child)
* processNodeWhileParsing(thing, whitespace text)
* processTagClose(thing)
)
The Element objects passed to those functions are the same ones you'd see; the tag open and tag close calls receive the same
object, so you can compare them with the `is` operator if you want.
The default behavior of each function is that `processTagOpen` and `processTagClose` do nothing.
`processNodeWhileParsing`'s default behavior is to call `parent.appendChild(child)`, in order to
build the dom tree. If you do not want the dom tree, you can do override this function to do nothing.
If you do not choose to append child to parent in `processNodeWhileParsing`, the garbage collector is free to clean up
the node even as the document is not finished parsing, allowing memory use to stay lower. Memory use will tend to scale
approximately with the max depth in the element tree rather the entire document size.
To cancel processing before the end of a document, you'll have to throw an exception and catch it at your call to parse.
There is no other way to stop early and there are no concrete plans to add one.
There are several approaches to use this: you might might use `processTagOpen` and `processTagClose` to keep a stack or
other state variables to process nodes as they come and never add them to the actual tree. You might also build partial
subtrees to use all the convenient methods in `processTagClose`, but then not add that particular node to the rest of the
tree to keep memory usage down.
Examples:
Suppose you have a large array of items under the root element you'd like to process individually, without
taking all the items into memory at once. You can do that with code like this:
---
import arsd.dom;
class MyStream : XmlDocument {
this(string s) { super(s); } // need to forward the constructor we use
override void processNodeWhileParsing(Element parent, Element child) {
// don't append anything to the root node, since we don't need them
// all in the tree - that'd take too much memory -
// but still build any subtree for each individual item for ease of processing
if(parent is root)
return;
else
super.processNodeWhileParsing(parent, child);
}
int count;
override void processTagClose(Element element) {
if(element.tagName == "item") {
// process the element here with all the regular dom functions on `element`
count++;
// can still use dom functions on the subtree we built
assert(element.requireSelector("name").textContent == "sample");
}
}
}
void main() {
// generate an example file with a million items
string xml = "<list>";
foreach(i; 0 .. 1_000_000) {
xml ~= "<item><name>sample</name><type>example</type></item>";
}
xml ~= "</list>";
auto document = new MyStream(xml);
assert(document.count == 1_000_000);
}
---
This example runs in about 1/10th of the memory and 2/3 of the time on my computer relative to a default [XmlDocument] full tree dom.
By overriding these three functions to fit the specific document and processing requirements you have, you might realize even bigger
gains over the normal full document tree while still getting most the benefits of the convenient dom functions.
Tip: if you use a [Utf8Stream] instead of a string, you might be able to bring the memory use further down. The easiest way to do that
is something like this when loading from a file:
---
import std.stdio;
auto file = File("filename.xml", "rb");
auto textStream = new Utf8Stream(() {
// get more
auto buffer = new char[](32 * 1024);
return cast(string) file.rawRead(buffer);
}, () {
// has more
return !file.eof;
});
auto document = new XmlDocument(textStream);
---
You'll need to forward a constructor in your subclasses that takes `Utf8Stream` too if you want to subclass to override the streaming parsing functions.
Note that if you do save parts of the document strings or objects, it might prevent the GC from freeing that string block anyway, since dom.d will often slice into its buffer while parsing instead of copying strings. It will depend on your specific case to know if this actually saves memory or not for you.
Bugs:
Even if you use a [Utf8Stream] to feed data and decline to append to the tree, the entire xml text is likely to
end up in memory anyway.
See_Also:
[Document#examples]'s high level streaming example.
History:
`processNodeWhileParsing` was added January 6, 2023.
`processTagOpen` and `processTagClose` were added February 21, 2025.
+/
void processTagOpen(Element what) {
}
/// ditto
void processTagClose(Element what) {
}
/// ditto
void processNodeWhileParsing(Element parent, Element child) {
parent.appendChild(child);
}
@ -548,14 +693,10 @@ class Document : FileResource, DomParent {
loose = !caseSensitive;
bool sawImproperNesting = false;
bool paragraphHackfixRequired = false;
bool nonNestableHackRequired = false;
int getLineNumber(sizediff_t p) {
int line = 1;
foreach(c; data[0..p])
if(c == '\n')
line++;
return line;
return data.getLineNumber(p);
}
void parseError(string message) {
@ -572,6 +713,9 @@ class Document : FileResource, DomParent {
}
string readTagName() {
data.markDataDiscardable(pos);
// remember to include : for namespaces
// basically just keep going until >, /, or whitespace
auto start = pos;
@ -957,7 +1101,7 @@ class Document : FileResource, DomParent {
}
string tagName = readTagName();
string[string] attributes;
AttributesHolder attributes;
Ele addTag(bool selfClosed) {
if(selfClosed)
@ -972,7 +1116,7 @@ class Document : FileResource, DomParent {
import std.algorithm.comparison;
if(strict) {
enforce(data[pos] == '>', format("got %s when expecting > (possible missing attribute name)\nContext:\n%s", data[pos], data[max(0, pos - 100) .. min(data.length, pos + 100)]));
enforce(data[pos] == '>', format("got %s when expecting > (possible missing attribute name)\nContext:\n%s", data[pos], data[max(0, pos - data.contextToKeep) .. min(data.length, pos + data.contextToKeep)]));
} else {
// if we got here, it's probably because a slash was in an
// unquoted attribute - don't trust the selfClosed value
@ -1002,6 +1146,15 @@ class Document : FileResource, DomParent {
e.selfClosed = selfClosed;
e.parseAttributes();
// might temporarily set root to the first element we encounter,
// then the final root element assignment will be at the end of the parse,
// when the recursive work is complete.
if(this.root is null)
this.root = e;
this.processTagOpen(e);
scope(exit)
this.processTagClose(e);
// HACK to handle script and style as a raw data section as it is in HTML browsers
if(!pureXmlMode && tagName.isInArray(rawSourceElements)) {
@ -1039,12 +1192,12 @@ class Document : FileResource, DomParent {
bool closed = selfClosed;
void considerHtmlParagraphHack(Element n) {
void considerHtmlNonNestableElementHack(Element n) {
assert(!strict);
if(e.tagName == "p" && e.tagName == n.tagName) {
if(!canNestElementsInHtml(e.tagName, n.tagName)) {
// html lets you write <p> para 1 <p> para 1
// but in the dom tree, they should be siblings, not children.
paragraphHackfixRequired = true;
nonNestableHackRequired = true;
}
}
@ -1066,7 +1219,7 @@ class Document : FileResource, DomParent {
piecesBeforeRoot ~= n.element;
} else if(n.type == 0) {
if(!strict)
considerHtmlParagraphHack(n.element);
considerHtmlNonNestableElementHack(n.element);
processNodeWhileParsing(e, n.element);
} else if(n.type == 1) {
bool found = false;
@ -1078,7 +1231,7 @@ class Document : FileResource, DomParent {
// this is so we don't drop several levels of awful markup
if(n.element) {
if(!strict)
considerHtmlParagraphHack(n.element);
considerHtmlNonNestableElementHack(n.element);
processNodeWhileParsing(e, n.element);
n.element = null;
}
@ -1095,6 +1248,17 @@ class Document : FileResource, DomParent {
return n;
}
/+
// COMMENTED OUT BLOCK
// dom.d used to replace improper close tags with their
// text so they'd be visible in the output. the html
// spec says to just ignore them, and browsers do indeed
// seem to jsut ignore them, even checking back on IE6.
// so i guess i was wrong to do this (tho tbh i find it kinda
// useful to call out an obvious mistake in the source...
// but for calling out obvious mistakes, just use strict
// mode.)
// if not, this is a text node; we can't fix it up...
// If it's already in the tree somewhere, assume it is closed by algorithm
@ -1115,11 +1279,13 @@ class Document : FileResource, DomParent {
if(!found) // if not found in the tree though, it's probably just text
processNodeWhileParsing(e, TextNode.fromUndecodedString(this, "</"~n.payload~">"));
+/
}
} else {
if(n.element) {
if(!strict)
considerHtmlParagraphHack(n.element);
considerHtmlNonNestableElementHack(n.element);
processNodeWhileParsing(e, n.element);
}
}
@ -1251,7 +1417,7 @@ class Document : FileResource, DomParent {
parseUtf8(`<html><head></head><body></body></html>`); // fill in a dummy document in loose mode since that's what browsers do
}
if(paragraphHackfixRequired) {
if(nonNestableHackRequired) {
assert(!strict); // this should never happen in strict mode; it ought to never set the hack flag...
// in loose mode, we can see some "bad" nesting (it's valid html, but poorly formed xml).
@ -1265,7 +1431,7 @@ class Document : FileResource, DomParent {
if(ele.parentNode is null)
continue;
if(ele.tagName == "p" && ele.parentNode.tagName == ele.tagName) {
if(!canNestElementsInHtml(ele.parentNode.tagName, ele.tagName)) {
auto shouldBePreviousSibling = ele.parentNode;
auto holder = shouldBePreviousSibling.parentNode; // this is the two element's mutual holder...
if (auto p = holder in insertLocations) {
@ -1776,6 +1942,26 @@ unittest {
auto xml = new XmlDocument(`<my-stuff>hello</my-stuff>`);
}
bool canNestElementsInHtml(string parentTagName, string childTagName) {
switch(parentTagName) {
case "p", "h1", "h2", "h3", "h4", "h5", "h6":
// only should include "phrasing content"
switch(childTagName) {
case "p", "dl", "dt", "dd", "h1", "h2", "h3", "h4", "h5", "h6":
return false;
default: return true;
}
case "dt", "dd":
switch(childTagName) {
case "dd", "dt":
return false;
default: return true;
}
default:
return true;
}
}
interface DomParent {
inout(Document) asDocument() inout;
inout(Element) asElement() inout;
@ -2275,6 +2461,7 @@ class Element : DomParent {
// do nothing, this is primarily a virtual hook
// for links and forms
void setValue(string field, string value) { }
void setValue(string field, string[] value) { }
// this is a thing so i can remove observer support if it gets slow
@ -2299,8 +2486,13 @@ class Element : DomParent {
/// The name of the tag. Remember, changing this doesn't change the dynamic type of the object.
string tagName;
/// This is where the attributes are actually stored. You should use getAttribute, setAttribute, and hasAttribute instead.
string[string] attributes;
/++
This is where the attributes are actually stored. You should use getAttribute, setAttribute, and hasAttribute instead.
History:
`AttributesHolder` replaced `string[string]` on August 22, 2024
+/
AttributesHolder attributes;
/// In XML, it is valid to write <tag /> for all elements with no children, but that breaks HTML, so I don't do it here.
/// Instead, this flag tells if it should be. It is based on the source document's notation and a html element list.
@ -2500,8 +2692,8 @@ class Element : DomParent {
/// Generally, you don't want to call this yourself - use Element.make or document.createElement instead.
this(Document _parentDocument, string _tagName, string[string] _attributes = null, bool _selfClosed = false) {
tagName = _tagName;
if(_attributes !is null)
attributes = _attributes;
foreach(k, v; _attributes)
attributes[k] = v;
selfClosed = _selfClosed;
version(dom_node_indexes)
@ -2522,8 +2714,8 @@ class Element : DomParent {
+/
this(string _tagName, string[string] _attributes = null, const string[] selfClosedElements = htmlSelfClosedElements) {
tagName = _tagName;
if(_attributes !is null)
attributes = _attributes;
foreach(k, v; _attributes)
attributes[k] = v;
selfClosed = tagName.isInArray(selfClosedElements);
// this is meant to reserve some memory. It makes a small, but consistent improvement.
@ -2828,7 +3020,7 @@ class Element : DomParent {
tag = tag.toLower();
Element[] ret;
foreach(e; tree)
if(e.tagName == tag)
if(e.tagName == tag || tag == "*")
ret ~= e;
return ret;
}
@ -2848,11 +3040,7 @@ class Element : DomParent {
string getAttribute(string name) const {
if(parentDocument && parentDocument.loose)
name = name.toLower();
auto e = name in attributes;
if(e)
return *e;
else
return null;
return attributes.get(name, null);
}
/**
@ -3058,7 +3246,7 @@ class Element : DomParent {
/* done */
_computedStyle = new CssStyle(null, style); // gives at least something to work with
_computedStyle = computedStyleFactory(this);
}
return _computedStyle;
}
@ -3317,6 +3505,15 @@ class Element : DomParent {
return stealChildren(d.root);
}
/++
Returns `this` for use inside `with` expressions.
History:
Added December 20, 2024
+/
inout(Element) self() inout pure @nogc nothrow @safe scope return {
return this;
}
/++
Inserts a child under this element after the element `where`.
@ -3999,9 +4196,18 @@ class Element : DomParent {
string writeTagOnly(Appender!string where = appender!string()) const {
+/
/// This is the actual implementation used by toString. You can pass it a preallocated buffer to save some time.
/// Note: the ordering of attributes in the string is undefined.
/// Returns the string it creates.
/++
This is the actual implementation used by toString. You can pass it a preallocated buffer to save some time.
Note: the ordering of attributes in the string is undefined.
Returns the string it creates.
Implementation_Notes:
The order of attributes printed by this function is undefined, as permitted by the XML spec. You should NOT rely on any implementation detail noted here.
However, in practice, between June 14, 2019 and August 22, 2024, it actually did sort attributes by key name. After August 22, 2024, it changed to track attribute append order and will print them back out in the order in which the keys were first seen.
This is subject to change again at any time. Use [toPrettyString] if you want a defined output (toPrettyString always sorts by name for consistent diffing).
+/
string writeToAppender(Appender!string where = appender!string()) const {
assert(tagName !is null);
@ -4012,10 +4218,13 @@ class Element : DomParent {
where.put("<");
where.put(tagName);
/+
import std.algorithm : sort;
auto keys = sort(attributes.keys);
foreach(n; keys) {
auto v = attributes[n]; // I am sorting these for convenience with another project. order of AAs is undefined, so I'm allowed to do it.... and it is still undefined, I might change it back later.
+/
foreach(n, v; attributes) {
//assert(v !is null);
where.put(" ");
where.put(n);
@ -4134,6 +4343,7 @@ class Element : DomParent {
}
}
// computedStyle could argubaly be removed to bring size down
//pragma(msg, __traits(classInstanceSize, Element));
//pragma(msg, Element.tupleof);
@ -4148,14 +4358,25 @@ class Element : DomParent {
+/
/// Group: core_functionality
class XmlDocument : Document {
/++
Constructs a stricter-mode XML parser and parses the given data source.
History:
The `Utf8Stream` version of the constructor was added on February 22, 2025.
+/
this(string data, bool enableHtmlHacks = false) {
this(new Utf8Stream(data), enableHtmlHacks);
}
/// ditto
this(Utf8Stream data, bool enableHtmlHacks = false) {
selfClosedElements = null;
inlineElements = null;
rawSourceElements = null;
contentType = "text/xml; charset=utf-8";
_prolog = `<?xml version="1.0" encoding="UTF-8"?>` ~ "\n";
parseStrict(data, !enableHtmlHacks);
parseStream(data, true, true, !enableHtmlHacks);
}
}
@ -4432,7 +4653,258 @@ struct AttributeSet {
mixin JavascriptStyleDispatch!();
}
private struct InternalAttribute {
// variable length structure
private InternalAttribute* next;
private uint totalLength;
private ushort keyLength;
private char[0] chars;
// this really should be immutable tbh
inout(char)[] key() inout return {
return chars.ptr[0 .. keyLength];
}
inout(char)[] value() inout return {
return chars.ptr[keyLength .. totalLength];
}
static InternalAttribute* make(in char[] key, in char[] value) {
// old code was
//auto data = new ubyte[](InternalAttribute.sizeof + key.length + value.length);
//GC.addRange(data.ptr, data.length); // MUST add the range to scan it!
import core.memory;
// but this code is a bit better, notice we did NOT set the NO_SCAN attribute because of the presence of the next pointer
// (this can sometimes be a pessimization over the separate strings but meh, most of these attributes are supposed to be small)
auto obj = cast(InternalAttribute*) GC.calloc(InternalAttribute.sizeof + key.length + value.length);
// assert(key.length > 0);
obj.totalLength = cast(uint) (key.length + value.length);
obj.keyLength = cast(ushort) key.length;
if(key.length != obj.keyLength)
throw new Exception("attribute key overflow");
if(key.length + value.length != obj.totalLength)
throw new Exception("attribute length overflow");
obj.key[] = key[];
obj.value[] = value[];
return obj;
}
// FIXME: disable default ctor and op new
}
import core.exception;
struct AttributesHolder {
private @system InternalAttribute* attributes;
/+
invariant() {
const(InternalAttribute)* wtf = attributes;
while(wtf) {
assert(wtf != cast(void*) 1);
assert(wtf.keyLength != 0);
import std.stdio; writeln(wtf.key, "=", wtf.value);
wtf = wtf.next;
}
}
+/
/+
It is legal to do foo["key", "default"] to call it with no error...
+/
string opIndex(scope const char[] key) const {
auto found = find(key);
if(found is null)
throw new RangeError(key.idup); // FIXME
return cast(string) found.value;
}
string get(scope const char[] key, string returnedIfKeyNotFound = null) const {
auto attr = this.find(key);
if(attr is null)
return returnedIfKeyNotFound;
else
return cast(string) attr.value;
}
private string[] keys() const {
string[] ret;
foreach(k, v; this)
ret ~= k;
return ret;
}
/+
If this were to return a string* it'd be tricky cuz someone could try to rebind it, which is impossible.
This is a breaking change. You can get a similar result though with [get].
+/
bool opBinaryRight(string op : "in")(scope const char[] key) const {
return find(key) !is null;
}
private inout(InternalAttribute)* find(scope const char[] key) inout @trusted {
inout(InternalAttribute)* current = attributes;
while(current) {
// assert(current > cast(void*) 1);
if(current.key == key)
return current;
current = current.next;
}
return null;
}
void remove(scope const char[] key) @trusted {
if(attributes is null)
return;
auto current = attributes;
InternalAttribute* previous;
while(current) {
if(current.key == key)
break;
previous = current;
current = current.next;
}
if(current is null)
return;
if(previous is null)
attributes = current.next;
else
previous.next = current.next;
// assert(previous.next != cast(void*) 1);
// assert(attributes != cast(void*) 1);
}
void opIndexAssign(scope const char[] value, scope const char[] key) @trusted {
if(attributes is null) {
attributes = InternalAttribute.make(key, value);
return;
}
auto current = attributes;
if(current.key == key) {
if(current.value != value) {
auto replacement = InternalAttribute.make(key, value);
attributes = replacement;
replacement.next = current.next;
// assert(replacement.next != cast(void*) 1);
// assert(attributes != cast(void*) 1);
}
return;
}
while(current.next) {
if(current.next.key == key) {
if(current.next.value == value)
return; // replacing immutable value with self, no change
break;
}
current = current.next;
}
assert(current !is null);
auto replacement = InternalAttribute.make(key, value);
if(current.next !is null)
replacement.next = current.next.next;
current.next = replacement;
// assert(current.next != cast(void*) 1);
// assert(replacement.next != cast(void*) 1);
}
int opApply(int delegate(string key, string value) dg) const @trusted {
const(InternalAttribute)* current = attributes;
while(current !is null) {
if(auto res = dg(cast(string) current.key, cast(string) current.value))
return res;
current = current.next;
}
return 0;
}
string toString() {
string ret;
foreach(k, v; this) {
if(ret.length)
ret ~= " ";
ret ~= k;
ret ~= `="`;
ret ~= v;
ret ~= `"`;
}
return ret;
}
}
unittest {
AttributesHolder holder;
holder["one"] = "1";
holder["two"] = "2";
holder["three"] = "3";
{
assert("one" in holder);
assert("two" in holder);
assert("three" in holder);
assert("four" !in holder);
int count;
foreach(k, v; holder) {
switch(count) {
case 0: assert(k == "one" && v == "1"); break;
case 1: assert(k == "two" && v == "2"); break;
case 2: assert(k == "three" && v == "3"); break;
default: assert(0);
}
count++;
}
}
holder["two"] = "dos";
{
assert("one" in holder);
assert("two" in holder);
assert("three" in holder);
assert("four" !in holder);
int count;
foreach(k, v; holder) {
switch(count) {
case 0: assert(k == "one" && v == "1"); break;
case 1: assert(k == "two" && v == "dos"); break;
case 2: assert(k == "three" && v == "3"); break;
default: assert(0);
}
count++;
}
}
holder["four"] = "4";
{
assert("one" in holder);
assert("two" in holder);
assert("three" in holder);
assert("four" in holder);
int count;
foreach(k, v; holder) {
switch(count) {
case 0: assert(k == "one" && v == "1"); break;
case 1: assert(k == "two" && v == "dos"); break;
case 2: assert(k == "three" && v == "3"); break;
case 3: assert(k == "four" && v == "4"); break;
default: assert(0);
}
count++;
}
}
}
/// for style, i want to be able to set it with a string like a plain attribute,
/// but also be able to do properties Javascript style.
@ -4441,10 +4913,20 @@ struct AttributeSet {
struct ElementStyle {
this(Element parent) {
_element = parent;
_attribute = _element.getAttribute("style");
originalAttribute = _attribute;
}
~this() {
if(_attribute !is originalAttribute)
_element.setAttribute("style", _attribute);
}
Element _element;
string _attribute;
string originalAttribute;
/+
@property ref inout(string) _attribute() inout {
auto s = "style" in _element.attributes;
if(s is null) {
@ -4456,6 +4938,7 @@ struct ElementStyle {
assert(s !is null);
return *s;
}
+/
alias _attribute this; // this is meant to allow element.style = element.style ~ " string "; to still work.
@ -5454,6 +5937,10 @@ class Link : Element {
updateQueryString(vars);
}
override void setValue(string name, string[] variable) {
assert(0, "not implemented FIXME");
}
/// Removes the given variable from the query string
void removeValue(string name) {
auto vars = variablesHash();
@ -5525,6 +6012,10 @@ class Form : Element {
setValue(field, value, true);
}
override void setValue(string name, string[] variable) {
assert(0, "not implemented FIXME");
}
// FIXME: doesn't handle arrays; multiple fields can have the same name
/// Set's the form field's value. For input boxes, this sets the value attribute. For
@ -7147,7 +7638,8 @@ int intFromHex(string hex) {
break;
default:
assert(0, token);
import arsd.core;
throw ArsdException!"CSS Selector Problem"(token, tokens, cast(int) state);
}
}
break;
@ -7197,6 +7689,9 @@ int intFromHex(string hex) {
case "root":
current.rootElement = true;
break;
case "lang":
state = State.SkippingFunctionalSelector;
continue;
case "nth-child":
current.nthChild ~= ParsedNth(readFunctionalSelector());
state = State.SkippingFunctionalSelector;
@ -7209,6 +7704,11 @@ int intFromHex(string hex) {
current.nthLastOfType ~= ParsedNth(readFunctionalSelector());
state = State.SkippingFunctionalSelector;
continue;
case "nth-last-child":
// FIXME
//current.nthLastOfType ~= ParsedNth(readFunctionalSelector());
state = State.SkippingFunctionalSelector;
continue;
case "is":
state = State.SkippingFunctionalSelector;
current.isSelectors ~= readFunctionalSelector();
@ -7358,6 +7858,24 @@ Element[] removeDuplicates(Element[] input) {
// done with CSS selector handling
/++
This delegate is called if you call [Element.computedStyle] to attach an object to the element
that holds stylesheet information. You can rebind it to something else to return a subclass
if you want to hold more per-element extension data than the normal computed style object holds
(e.g. layout info as well).
The default is `return new CssStyle(null, element.style);`
History:
Added September 13, 2024 (dub v11.6)
+/
CssStyle function(Element e) computedStyleFactory = &defaultComputedStyleFactory;
/// ditto
CssStyle defaultComputedStyleFactory(Element e) {
return new CssStyle(null, e.style); // gives at least something to work with
}
// FIXME: use the better parser from html.d
/// This is probably not useful to you unless you're writing a browser or something like that.
@ -7392,6 +7910,7 @@ class CssStyle {
p.specificity = originatingSpecificity;
properties ~= p;
}
foreach(property; properties)
@ -7402,8 +7921,19 @@ class CssStyle {
Specificity getSpecificityOfRule(string rule) {
Specificity s;
if(rule.length == 0) { // inline
// s.important = 2;
s.important = 2;
} else {
// SO. WRONG.
foreach(ch; rule) {
if(ch == '.')
s.classes++;
if(ch == '#')
s.ids++;
if(ch == ' ')
s.tags++;
if(ch == ',')
break;
}
// FIXME
}
@ -7444,7 +7974,7 @@ class CssStyle {
if(value is null)
return getValue(name);
else
return setValue(name, value, 0x02000000 /* inline specificity */);
return setValue(name, value, Specificity(0x02000000) /* inline specificity */);
}
/// takes dash style name
@ -7468,6 +7998,7 @@ class CssStyle {
if(newSpecificity.score >= property.specificity.score) {
property.givenExplicitly = explicit;
expandShortForm(property, newSpecificity);
property.specificity = newSpecificity;
return (property.value = value);
} else {
if(name == "display")
@ -7519,7 +8050,7 @@ class CssStyle {
setValue(name ~"-left", parts[3], specificity, false);
break;
default:
assert(0, value);
// assert(0, value);
}
}
@ -7846,6 +8377,13 @@ private string[string] aadup(in string[string] arr) {
return ret;
}
private AttributesHolder aadup(const AttributesHolder arr) {
AttributesHolder ret;
foreach(k, v; arr)
ret[k] = v;
return ret;
}
@ -8349,25 +8887,53 @@ class Utf8Stream {
// stdout.flush();
}
enum contextToKeep = 100;
void markDataDiscardable(size_t p) {
if(p < contextToKeep)
return;
p -= contextToKeep;
// pretends data[0 .. p] is gone and adjusts future things as if it was still there
startingLineNumber = getLineNumber(p);
assert(p >= virtualStartIndex);
data = data[p - virtualStartIndex .. $];
virtualStartIndex = p;
}
int getLineNumber(size_t p) {
int line = startingLineNumber;
assert(p >= virtualStartIndex);
foreach(c; data[0 .. p - virtualStartIndex])
if(c == '\n')
line++;
return line;
}
@property final size_t length() {
// the parser checks length primarily directly before accessing the next character
// so this is the place we'll hook to append more if possible and needed.
if(lastIdx + 1 >= data.length && hasMore()) {
if(lastIdx + 1 >= (data.length + virtualStartIndex) && hasMore()) {
data ~= getMore();
}
return data.length;
return data.length + virtualStartIndex;
}
final char opIndex(size_t idx) {
if(idx > lastIdx)
lastIdx = idx;
return data[idx];
return data[idx - virtualStartIndex];
}
final string opSlice(size_t start, size_t end) {
if(end > lastIdx)
lastIdx = end;
return data[start .. end];
// writeln(virtualStartIndex, " " , start, " ", end);
assert(start >= virtualStartIndex);
assert(end >= virtualStartIndex);
return data[start - virtualStartIndex .. end - virtualStartIndex];
}
final size_t opDollar() {
@ -8396,6 +8962,9 @@ class Utf8Stream {
bool delegate() hasMoreHelper;
string delegate() getMoreHelper;
int startingLineNumber = 1;
size_t virtualStartIndex = 0;
/+
// used to maybe clear some old stuff
@ -8420,11 +8989,13 @@ void fillForm(T)(Form form, T obj, string name) {
History:
Added March 25, 2022 (dub v10.8)
The `stripLeadingAndTrailing` argument was added September 13, 2024 (dub v11.6).
+/
string normalizeWhitespace(string text) {
string normalizeWhitespace(string text, bool stripLeadingAndTrailing = true) {
string ret;
ret.reserve(text.length);
bool lastWasWhite = true;
bool lastWasWhite = stripLeadingAndTrailing;
foreach(char ch; text) {
if(ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') {
if(lastWasWhite)
@ -8438,12 +9009,23 @@ string normalizeWhitespace(string text) {
ret ~= ch;
}
return ret.stripRight;
if(stripLeadingAndTrailing)
return ret.stripRight;
else {
/+
if(lastWasWhite && (ret.length == 0 || ret[$-1] != ' '))
ret ~= ' ';
+/
return ret;
}
}
unittest {
assert(normalizeWhitespace(" foo ") == "foo");
assert(normalizeWhitespace(" f\n \t oo ") == "f oo");
assert(normalizeWhitespace(" foo ", false) == " foo ");
assert(normalizeWhitespace(" foo ", false) == " foo ");
assert(normalizeWhitespace("\nfoo", false) == " foo");
}
unittest {

View File

@ -28,12 +28,12 @@
"configurations": [
{
"name": "normal",
"libs-windows": ["gdi32", "ole32"]
"libs-windows": ["dwmapi", "gdi32", "ole32"]
},
{
"name": "without-opengl",
"versions": ["without_opengl"],
"libs-windows": ["gdi32", "ole32"]
"libs-windows": ["dwmapi", "gdi32", "ole32"]
},
{
"name": "cocoa",
@ -70,12 +70,12 @@
"bindbc-freetype": {
"version": "*",
"optional": true,
"optional": true
},
"bindbc-opengl": {
"version": "*",
"optional": true,
},
"optional": true
}
},
"libs-posix": ["freetype", "fontconfig"],
"sourceFiles": ["nanovega.d", "blendish.d"]
@ -178,6 +178,7 @@
"arsd-official:core":"*",
"arsd-official:minigui":"*",
"arsd-official:joystick":"*",
"arsd-official:ttf":"*",
"arsd-official:simpleaudio":"*",
"arsd-official:gamehelpers":"*"
},
@ -694,6 +695,18 @@
"dflags-ldc": ["--mv=arsd.pixmappaint=$PACKAGE_DIR/pixmappaint.d"],
"dflags-gdc": ["-fmodule-file=arsd.pixmappaint=$PACKAGE_DIR/pixmappaint.d"]
},
{
"name": "pixmaprecorder",
"description": "Video rendering extension for Pixmap Paint. Fancy wrapper for piping frame data to FFmpeg.",
"targetType": "library",
"sourceFiles": ["pixmaprecorder.d"],
"dependencies": {
"arsd-official:pixmappaint":"*"
},
"dflags-dmd": ["-mv=arsd.pixmaprecorder=$PACKAGE_DIR/pixmaprecorder.d"],
"dflags-ldc": ["--mv=arsd.pixmaprecorder=$PACKAGE_DIR/pixmaprecorder.d"],
"dflags-gdc": ["-fmodule-file=arsd.pixmaprecorder=$PACKAGE_DIR/pixmaprecorder.d"]
},
{
"name": "pixmappresenter",
"description": "High-level display library. Designed to blit fully-rendered frames to the screen.",
@ -774,6 +787,18 @@
"dflags-dmd": ["-mv=arsd.archive=$PACKAGE_DIR/archive.d"],
"dflags-ldc": ["--mv=arsd.archive=$PACKAGE_DIR/archive.d"],
"dflags-gdc": ["-fmodule-file=arsd.archive=$PACKAGE_DIR/archive.d"]
},
{
"name": "ini",
"description": "INI configuration file support - configurable INI parser and serializer with support for various dialects.",
"targetType": "library",
"sourceFiles": ["ini.d"],
"dependencies": {
"arsd-official:core":"*"
},
"dflags-dmd": ["-mv=arsd.ini=$PACKAGE_DIR/ini.d"],
"dflags-ldc": ["--mv=arsd.ini=$PACKAGE_DIR/ini.d"],
"dflags-gdc": ["-fmodule-file=arsd.ini=$PACKAGE_DIR/ini.d"]
}
]
}

70
email.d
View File

@ -14,7 +14,6 @@
module arsd.email;
import std.net.curl;
pragma(lib, "curl");
import std.base64;
import std.string;
@ -25,6 +24,8 @@ import std.algorithm.iteration;
import arsd.characterencodings;
public import arsd.core : FilePath;
// import std.uuid;
// smtpMessageBoundary = randomUUID().toString();
@ -457,15 +458,56 @@ class EmailMessage {
const(MimeAttachment)[] attachments;
/++
The filename is what is shown to the user, not the file on your sending computer. It should NOT have a path in it.
The attachmentFileName is what is shown to the user, not the file on your sending computer. It should NOT have a path in it.
If you want a filename from your computer, try [addFileAsAttachment].
The `mimeType` can be excluded if the filename has a common extension supported by the library.
---
message.addAttachment("text/plain", "something.txt", std.file.read("/path/to/local/something.txt"));
---
History:
The overload without `mimeType` was added October 28, 2024.
The parameter `attachmentFileName` was previously called `filename`. This was changed for clarity and consistency with other overloads on October 28, 2024.
+/
void addAttachment(string mimeType, string filename, const void[] content, string id = null) {
void addAttachment(string mimeType, string attachmentFileName, const void[] content, string id = null) {
isMime = true;
attachments ~= MimeAttachment(mimeType, filename, cast(const(ubyte)[]) content, id);
attachments ~= MimeAttachment(mimeType, attachmentFileName, cast(const(ubyte)[]) content, id);
}
/// ditto
void addAttachment(string attachmentFileName, const void[] content, string id = null) {
import arsd.core;
addAttachment(FilePath(attachmentFileName).contentTypeFromFileExtension, attachmentFileName, content, id);
}
/++
Reads the local file and attaches it.
If `attachmentFileName` is null, it uses the filename of `localFileName`, without the directory.
If `mimeType` is null, it guesses one based on the local file name's file extension.
If these cannot be determined, it will throw an `InvalidArgumentsException`.
History:
Added October 28, 2024
+/
void addFileAsAttachment(FilePath localFileName, string attachmentFileName = null, string mimeType = null, string id = null) {
if(mimeType is null)
mimeType = localFileName.contentTypeFromFileExtension;
if(attachmentFileName is null)
attachmentFileName = localFileName.filename;
import std.file;
addAttachment(mimeType, attachmentFileName, std.file.read(localFileName.toString()), id);
// see also: curl.h :1877 CURLOPT(CURLOPT_XOAUTH2_BEARER, CURLOPTTYPE_STRINGPOINT, 220),
// also option to force STARTTLS
}
/// in the html, use img src="cid:ID_GIVEN_HERE"
@ -497,7 +539,7 @@ class EmailMessage {
if(to.length)
headers ~= "To: " ~ to.toProtocolString(this.linesep);
if(cc.length)
headers ~= "Cc: " ~ to.toProtocolString(this.linesep);
headers ~= "Cc: " ~ cc.toProtocolString(this.linesep);
if(from.length)
headers ~= "From: " ~ from.toProtocolString(this.linesep);
@ -705,7 +747,7 @@ class MimePart {
MimeAttachment att;
att.type = type;
if(att.type == "application/octet-stream" && filename.length == 0 && name.length > 0 ) {
if(filename.length == 0 && name.length > 0 ) {
att.filename = name;
} else {
att.filename = filename;
@ -1176,11 +1218,17 @@ class IncomingEmailMessage : EmailMessage {
break;
case "multipart/mixed":
if(part.stuff.length) {
auto msg = part.stuff[0];
foreach(thing; part.stuff[1 .. $]) {
attachments ~= thing.toMimeAttachment();
MimePart msg;
foreach(idx, thing; part.stuff) {
if(msg is null && thing.disposition != "attachment" && (thing.type.length == 0 || thing.type.indexOf("multipart/") != -1 || thing.type.indexOf("text/") != -1)) {
// the message should be the first suitable item for conversion
msg = thing;
} else {
attachments ~= thing.toMimeAttachment();
}
}
part = msg;
if(msg)
part = msg;
goto deeperInTheMimeTree;
}
@ -1611,7 +1659,7 @@ unittest {
assert(result.subject.equal(mail.subject));
assert(mail.to.canFind(result.to));
assert(result.from == mail.from.toString);
assert(result.from == mail.from.toProtocolString);
// This roundtrip works modulo trailing newline on the parsed message and LF vs CRLF
assert(result.textMessageBody.replace("\n", "\r\n").stripRight().equal(mail.textBody_));

144
game.d
View File

@ -191,7 +191,7 @@
* [Nanovega|arsd.nanovega] 2d vector graphics. Nanovega supports its own text drawing functions.
* The `BasicDrawing` functions provided by `arsd.game`. To some extent, you'll be able to mix and match these with other drawing models. It is just bare minimum functionality you might find useful made in a more concise form than even old-style opengl.
* The `BasicDrawing` functions provided by `arsd.game`. To some extent, you'll be able to mix and match these with other drawing models. It is just bare minimum functionality you might find useful made in a more concise form than even old-style opengl or for porting something that uses a ScreenPainter. (not implemented)
)
Please note that the simpledisplay ScreenPainter will NOT work in a game `drawFrame` function.
@ -224,7 +224,7 @@
$(H2 Random numbers)
std.random works but might want another thing so the seed is saved with the game.
std.random works but might want another thing so the seed is saved with the game. An old school trick is to seed it based on some user input, even just time it took then to go past the title screen.
$(H2 Screenshots)
@ -285,7 +285,7 @@
Most computer programs are written either as batch processors or as event-driven applications. Batch processors do their work when requested, then exit. Event-driven applications, including many video games, wait for something to happen, like the user pressing a key or clicking the mouse, respond to it, then go back to waiting. These might do some animations, but this is the exception to its run time, not the rule. You are assumed to be waiting for events, but can `requestAnimationFrame` for the special occasions.
But this is the rule for the third category of programs: time-driven programs, and many video games fall into this category. This is what `arsd.game` tries to make easy. It assumes you want a timed `update` and a steady stream of animation frames, and if you want to make an exception, you can pause updates until an event comes in. FIXME: `pauseUntilNextInput`.
But this is the rule for the third category of programs: time-driven programs, and many video games fall into this category. This is what `arsd.game` tries to make easy. It assumes you want a timed `update` and a steady stream of animation frames, and if you want to make an exception, you can pause updates until an event comes in. FIXME: `pauseUntilNextInput`. `designFps` = 0, `requestAnimationFrame`, `requestAnimation(duration)`
$(H3 Webassembly implementation)
@ -570,29 +570,33 @@ public import core.time;
import arsd.core;
import arsd.simpledisplay : Timer;
public import arsd.joystick;
/++
Creates a simple 2d opengl simpledisplay window. It sets the matrix for pixel coordinates and enables alpha blending and textures.
Creates a simple 2d (old-style) opengl simpledisplay window. It sets the matrix for pixel coordinates and enables alpha blending and textures.
+/
SimpleWindow create2dWindow(string title, int width = 512, int height = 512) {
auto window = new SimpleWindow(width, height, title, OpenGlOptions.yes);
window.setAsCurrentOpenGlContext();
//window.visibleForTheFirstTime = () {
window.setAsCurrentOpenGlContext();
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glClearColor(0,0,0,0);
glDepthFunc(GL_LEQUAL);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glClearColor(0,0,0,0);
glDepthFunc(GL_LEQUAL);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0, width, height, 0, 0, 1);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0, width, height, 0, 0, 1);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glDisable(GL_DEPTH_TEST);
glEnable(GL_TEXTURE_2D);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glDisable(GL_DEPTH_TEST);
glEnable(GL_TEXTURE_2D);
//};
window.windowResized = (newWidth, newHeight) {
int x, y, w, h;
@ -610,6 +614,7 @@ SimpleWindow create2dWindow(string title, int width = 512, int height = 512) {
y = 0;
}
window.setAsCurrentOpenGlContext();
glViewport(x, y, w, h);
window.redrawOpenGlSceneSoon();
};
@ -643,6 +648,7 @@ abstract class GameHelperBase {
currentScreen.drawFrame(interpolateToNextFrame);
}
// in frames
ushort snesRepeatRate() { return ushort.max; }
ushort snesRepeatDelay() { return snesRepeatRate(); }
@ -1536,3 +1542,109 @@ void clearOpenGlScreen(SimpleWindow window) {
}
/++
History:
Added August 26, 2024
+/
interface BasicDrawing {
void fillRectangle(Rectangle r, Color c);
void outlinePolygon(Point[] vertexes, Color c);
void drawText(Rectangle boundingBox, string text, Color c);
}
/++
NOT fully compatible with simpledisplay's screenpainter, but emulates some of its api.
I want it to be runtime swappable between the fancy opengl and a backup one for my remote X purposes.
+/
class ScreenPainterImpl : BasicDrawing {
Color outlineColor;
Color fillColor;
import arsd.ttf;
SimpleWindow window;
OpenGlLimitedFontBase!() font;
this(SimpleWindow window, OpenGlLimitedFontBase!() font) {
this.window = window;
this.font = font;
}
void clear(Color c) {
fillRectangle(Rectangle(Point(0, 0), Size(window.width, window.height)), c);
}
void drawRectangle(Rectangle r) {
fillRectangle(r, fillColor);
Point[4] vertexes = [
r.upperLeft,
r.upperRight,
r.lowerRight,
r.lowerLeft
];
outlinePolygon(vertexes[], outlineColor);
}
void drawRectangle(Point ul, Size sz) {
drawRectangle(Rectangle(ul, sz));
}
void drawText(Point upperLeft, scope const char[] text) {
drawText(Rectangle(upperLeft, Size(4096, 4096)), text, outlineColor);
}
void fillRectangle(Rectangle r, Color c) {
glBegin(GL_QUADS);
glColor4f(c.r / 255.0, c.g / 255.0, c.b / 255.0, c.a / 255.0);
with(r) {
glVertex2i(upperLeft.x, upperLeft.y);
glVertex2i(upperRight.x, upperRight.y);
glVertex2i(lowerRight.x, lowerRight.y);
glVertex2i(lowerLeft.x, lowerLeft.y);
}
glEnd();
}
void outlinePolygon(Point[] vertexes, Color c) {
glBegin(GL_LINE_LOOP);
glColor4f(c.r / 255.0, c.g / 255.0, c.b / 255.0, c.a / 255.0);
foreach(vertex; vertexes) {
glVertex2i(vertex.x, vertex.y);
}
glEnd();
}
void drawText(Rectangle boundingBox, scope const char[] text, Color color) {
font.drawString(boundingBox.upperLeft.tupleof, text, color);
}
protected int refcount;
void flush() {
}
}
struct ScreenPainter {
ScreenPainterImpl impl;
this(ScreenPainterImpl impl) {
this.impl = impl;
impl.refcount++;
}
this(this) {
if(impl)
impl.refcount++;
}
~this() {
if(impl)
if(--impl.refcount == 0)
impl.flush();
}
alias impl this;
}

57
http2.d
View File

@ -1134,8 +1134,8 @@ class HttpRequest {
size_t bodyBytesReceived;
State state_;
State state() { return state_; }
State state(State s) {
final State state() { return state_; }
final State state(State s) {
assert(state_ != State.complete);
return state_ = s;
}
@ -2018,7 +2018,7 @@ class HttpRequest {
request.state = State.aborted;
request.responseData.code = 3;
request.responseData.codeText = "send failed to server";
request.responseData.codeText = "send failed to server: " ~ lastSocketError(sock);
inactive[inactiveCount++] = sock;
sock.close();
loseSocket(request.requestParameters.host, request.requestParameters.port, request.requestParameters.ssl, sock);
@ -2047,7 +2047,7 @@ class HttpRequest {
request.state = State.aborted;
request.responseData.code = 3;
request.responseData.codeText = "receive error from server";
request.responseData.codeText = "receive error from server: " ~ lastSocketError(sock);
}
inactive[inactiveCount++] = sock;
sock.close();
@ -3517,6 +3517,15 @@ void main() {
writeln(HttpRequest.socketsPerHost);
}
string lastSocketError(Socket sock) {
import std.socket;
version(use_openssl) {
if(auto s = cast(OpenSslSocket) sock)
if(s.lastSocketError.length)
return s.lastSocketError;
}
return std.socket.lastSocketError();
}
// From sslsocket.d, but this is the maintained version!
version(use_openssl) {
@ -3963,7 +3972,7 @@ version(use_openssl) {
return 0;
}
bool dataPending() {
final bool dataPending() {
return OpenSSL.SSL_pending(ssl) > 0;
}
@ -3975,6 +3984,8 @@ version(use_openssl) {
}
}
private string lastSocketError;
@trusted
// returns true if it is finished, false if it would have blocked, throws if there's an error
int do_ssl_connect() {
@ -3987,12 +3998,12 @@ version(use_openssl) {
string str;
OpenSSL.ERR_print_errors_cb(&collectSslErrors, &str);
int i;
auto err = OpenSSL.SSL_get_verify_result(ssl);
//printf("wtf\n");
//scanf("%d\n", i);
this.lastSocketError = str ~ " " ~ getOpenSslErrorCode(err);
throw new Exception("Secure connect failed: " ~ getOpenSslErrorCode(err));
}
} else this.lastSocketError = null;
return 0;
}
@ -4005,18 +4016,13 @@ version(use_openssl) {
// don't need to throw anymore since it is checked elsewhere
// code useful sometimes for debugging hence commenting instead of deleting
version(none)
if(retval == -1) {
string str;
OpenSSL.ERR_print_errors_cb(&collectSslErrors, &str);
int i;
this.lastSocketError = str;
//printf("wtf\n");
//scanf("%d\n", i);
throw new Exception("ssl send failed " ~ str);
}
// throw new Exception("ssl send failed " ~ str);
} else this.lastSocketError = null;
return retval;
}
@ -4032,18 +4038,14 @@ version(use_openssl) {
// don't need to throw anymore since it is checked elsewhere
// code useful sometimes for debugging hence commenting instead of deleting
version(none)
if(retval == -1) {
string str;
OpenSSL.ERR_print_errors_cb(&collectSslErrors, &str);
int i;
this.lastSocketError = str;
//printf("wtf\n");
//scanf("%d\n", i);
throw new Exception("ssl receive failed " ~ str);
}
// throw new Exception("ssl receive failed " ~ str);
} else this.lastSocketError = null;
return retval;
}
override ptrdiff_t receive(scope void[] buf) {
@ -4790,7 +4792,7 @@ class WebSocket {
while(remaining.length) {
auto r = socket.send(remaining);
if(r < 0)
throw new Exception(lastSocketError());
throw new Exception(lastSocketError(socket));
if(r == 0)
throw new Exception("unexpected connection termination");
remaining = remaining[r .. $];
@ -4805,7 +4807,7 @@ class WebSocket {
auto r = socket.receive(buffer[used.length .. $]);
if(r < 0)
throw new Exception(lastSocketError());
throw new Exception(lastSocketError(socket));
if(r == 0)
throw new Exception("unexpected connection termination");
//import std.stdio;writef("%s", cast(string) buffer[used.length .. used.length + r]);
@ -5463,7 +5465,7 @@ class WebSocket {
sock.onerror();
if(sock.onclose)
sock.onclose(CloseEvent(CloseEvent.StandardCloseCodes.abnormalClosure, "Connection lost", false, lastSocketError()));
sock.onclose(CloseEvent(CloseEvent.StandardCloseCodes.abnormalClosure, "Connection lost", false, lastSocketError(sock.socket)));
unregisterActiveSocket(sock);
sock.socket.close();
@ -5618,6 +5620,7 @@ class WebSocket {
activeSockets ~= s;
s.registered = true;
version(use_arsd_core) {
version(Posix)
s.unregisterToken = arsd.core.getThisThreadEventLoop().addCallbackOnFdReadable(s.socket.handle, new arsd.core.CallbackHelper(() { s.readyToRead(s); }));
}
}

3158
ini.d Normal file

File diff suppressed because it is too large Load Diff

BIN
libssh2.dll Normal file → Executable file

Binary file not shown.

BIN
libssh2.lib Normal file → Executable file

Binary file not shown.

9529
markdown.d Normal file

File diff suppressed because it is too large Load Diff

5095
minigui.d

File diff suppressed because it is too large Load Diff

View File

@ -59,7 +59,7 @@ class ColorPickerDialog : Dialog {
void delegate(Color) onOK;
this(Color current, void delegate(Color) onOK, Window owner) {
super(360, 460, "Color picker");
super(owner, 360, 460, "Color picker");
this.onOK = onOK;

View File

@ -543,6 +543,8 @@ class WebViewWidget_CEF : WebViewWidgetBase {
ev.mode = NotifyModes.NotifyNormal;
ev.detail = NotifyDetail.NotifyVirtual;
// sdpyPrintDebugString("Sending FocusIn");
trapXErrors( {
XSendEvent(XDisplayConnection.get, ozone, false, 0, cast(XEvent*) &ev);
});
@ -560,6 +562,8 @@ class WebViewWidget_CEF : WebViewWidgetBase {
ev.mode = NotifyModes.NotifyNormal;
ev.detail = NotifyDetail.NotifyNonlinearVirtual;
// sdpyPrintDebugString("Sending FocusOut");
trapXErrors( {
XSendEvent(XDisplayConnection.get, ozone, false, 0, cast(XEvent*) &ev);
});
@ -943,7 +947,7 @@ version(cef) {
try {
auto ptr = callback.passable();
browser.runOnWebView((wv) {
getOpenFileName((string name) {
getOpenFileName(wv.parentWindow, (string name) {
auto callback = RC!cef_file_dialog_callback_t(ptr);
auto list = libcef.string_list_alloc();
auto item = cef_string_t(name);
@ -1284,6 +1288,7 @@ version(cef) {
class MiniguiFocusHandler : CEF!cef_focus_handler_t {
override void on_take_focus(RC!(cef_browser_t) browser, int next) nothrow {
// sdpyPrintDebugString("taking");
browser.runOnWebView(delegate(wv) {
Widget f;
if(next) {
@ -1305,7 +1310,40 @@ version(cef) {
ev.focus(); // even this can steal focus from other parts of my application!
});
+/
//sdpyPrintDebugString("setting");
// sdpyPrintDebugString("setting");
// if either the parent window or the ozone window has the focus, we
// can redirect it to the input focus. CEF calls this method sometimes
// before setting the focus (where return 1 can override) and sometimes
// after... which is totally inappropriate for it to do but it does anyway
// and we want to undo the damage of this.
browser.runOnWebView((ev) {
arsd.simpledisplay.Window focus_window;
int revert_to_return;
XGetInputFocus(XDisplayConnection.get, &focus_window, &revert_to_return);
if(focus_window is ev.parentWindow.win.impl.window || focus_window is ev.ozone) {
// refocus our correct input focus
ev.parentWindow.win.focus();
XSync(XDisplayConnection.get, 0);
// and then tell the chromium thing it still has it
// so it will think it got it, lost it, then got it again
// and hopefully not try to get it again
XFocusChangeEvent eve;
eve.type = arsd.simpledisplay.EventType.FocusIn;
eve.display = XDisplayConnection.get;
eve.window = ev.ozone;
eve.mode = NotifyModes.NotifyNormal;
eve.detail = NotifyDetail.NotifyVirtual;
// sdpyPrintDebugString("Sending FocusIn hack here");
trapXErrors( {
XSendEvent(XDisplayConnection.get, ev.ozone, false, 0, cast(XEvent*) &eve);
});
}
});
return 1; // otherwise, cancel because this bullshit tends to steal focus from other applications and i never, ever, ever want that to happen.
// seems to happen because of race condition in it getting a focus event and then stealing the focus from the parent
@ -1314,10 +1352,13 @@ version(cef) {
// it also breaks its own pop up menus and drop down boxes to allow this! wtf
}
override void on_got_focus(RC!(cef_browser_t) browser) nothrow {
// sdpyPrintDebugString("got");
browser.runOnWebView((ev) {
// this sometimes steals from the app too but it is relatively acceptable
// steals when i mouse in from the side of the window quickly, but still
// i want the minigui state to match so i'll allow it
//if(ev.parentWindow) ev.parentWindow.focus();
ev.focus();
});
}

File diff suppressed because it is too large Load Diff

View File

@ -103,7 +103,9 @@
// always have a size that is a
// multiple of the internal
// resolution.
// The gentle reader might have noticed that the integer scaling will result
// → Also check out the
// `intHybrid` scaling mode.
// The gentle reader might have noticed that integer scaling will result
// in a padding/border area around the image for most window sizes.
// How about changing its color?
cfg.renderer.background = ColorF(Pixel.white);
@ -192,7 +194,12 @@ alias Pixmap = arsd.pixmappaint.Pixmap;
alias WindowResizedCallback = void delegate(Size);
// is the Timer class available on this platform?
private enum hasTimer = is(Timer == class);
private enum hasTimer = is(arsd.simpledisplay.Timer == class);
// resolve symbol clash on “Timer” (arsd.core vs arsd.simpledisplay)
static if (hasTimer) {
private alias Timer = arsd.simpledisplay.Timer;
}
// viewport math
private @safe pure nothrow @nogc {
@ -365,12 +372,6 @@ enum Scaling {
cssCover = cover, /// equivalent CSS: `object-fit: cover;`
}
///
enum ScalingFilter {
nearest, /// nearest neighbor → blocky/pixelish
linear, /// (bi-)linear interpolation → smooth/blurry
}
///
struct PresenterConfig {
Window window; ///
@ -390,7 +391,7 @@ struct PresenterConfig {
Scaling scaling = Scaling.keepAspectRatio;
/++
Filter
Scaling filter
+/
ScalingFilter filter = ScalingFilter.nearest;
@ -408,8 +409,26 @@ struct PresenterConfig {
///
static struct Window {
///
string title = "ARSD Pixmap Presenter";
///
Size size;
/++
Window corner style
$(NOTE
At the time of writing, this is only implemented on Windows.
It has no effect elsewhere for now but does no harm either.
Windows: Requires Windows 11 or later.
)
History:
Added September 10, 2024.
+/
CornerStyle corners = CornerStyle.rectangular;
}
}
@ -492,8 +511,6 @@ final class OpenGl3PixmapRenderer : PixmapRenderer {
private {
PresenterObjectsContainer* _poc;
bool _clear = true;
GLfloat[16] _vertices;
OpenGlShader _shader;
GLuint _vao;
@ -512,6 +529,7 @@ final class OpenGl3PixmapRenderer : PixmapRenderer {
public void setup(PresenterObjectsContainer* pro) {
_poc = pro;
_poc.window.suppressAutoOpenglViewport = true;
_poc.window.visibleForTheFirstTime = &this.visibleForTheFirstTime;
_poc.window.redrawOpenGlScene = &this.redrawOpenGlScene;
}
@ -528,16 +546,13 @@ final class OpenGl3PixmapRenderer : PixmapRenderer {
}
void redrawOpenGlScene() {
if (_clear) {
glClearColor(
_poc.config.renderer.background.r,
_poc.config.renderer.background.g,
_poc.config.renderer.background.b,
_poc.config.renderer.background.a
);
glClear(GL_COLOR_BUFFER_BIT);
_clear = false;
}
glClearColor(
_poc.config.renderer.background.r,
_poc.config.renderer.background.g,
_poc.config.renderer.background.b,
_poc.config.renderer.background.a
);
glClear(GL_COLOR_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, _texture);
@ -618,7 +633,7 @@ final class OpenGl3PixmapRenderer : PixmapRenderer {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
break;
case linear:
case bilinear:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
break;
@ -645,7 +660,6 @@ final class OpenGl3PixmapRenderer : PixmapRenderer {
glViewportPMP(viewport);
this.setupTexture();
_clear = true;
}
void redrawSchedule() {
@ -685,8 +699,6 @@ final class OpenGl1PixmapRenderer : PixmapRenderer {
private {
PresenterObjectsContainer* _poc;
bool _clear = true;
GLuint _texture = 0;
}
@ -703,6 +715,7 @@ final class OpenGl1PixmapRenderer : PixmapRenderer {
public void setup(PresenterObjectsContainer* poc) {
_poc = poc;
_poc.window.suppressAutoOpenglViewport = true;
_poc.window.visibleForTheFirstTime = &this.visibleForTheFirstTime;
_poc.window.redrawOpenGlScene = &this.redrawOpenGlScene;
}
@ -729,7 +742,7 @@ final class OpenGl1PixmapRenderer : PixmapRenderer {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
break;
case linear:
case bilinear:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
break;
@ -763,16 +776,13 @@ final class OpenGl1PixmapRenderer : PixmapRenderer {
}
void redrawOpenGlScene() {
if (_clear) {
glClearColor(
_poc.config.renderer.background.r,
_poc.config.renderer.background.g,
_poc.config.renderer.background.b,
_poc.config.renderer.background.a,
);
glClear(GL_COLOR_BUFFER_BIT);
_clear = false;
}
glClearColor(
_poc.config.renderer.background.r,
_poc.config.renderer.background.g,
_poc.config.renderer.background.b,
_poc.config.renderer.background.a,
);
glClear(GL_COLOR_BUFFER_BIT);
glBindTexture(GL_TEXTURE_2D, _texture);
glEnable(GL_TEXTURE_2D);
@ -815,8 +825,6 @@ final class OpenGl1PixmapRenderer : PixmapRenderer {
this.setupTexture();
this.setupMatrix();
_clear = true;
}
public void redrawSchedule() {
@ -828,6 +836,40 @@ final class OpenGl1PixmapRenderer : PixmapRenderer {
}
}
/+
/++
Purely software renderer
+/
final class SoftwarePixmapRenderer : PixmapRenderer {
private {
PresenterObjectsContainer* _poc;
}
public WantsOpenGl wantsOpenGl() @safe pure nothrow @nogc {
return WantsOpenGl(0);
}
public void setup(PresenterObjectsContainer* container) {
}
public void reconfigure() {
}
/++
Schedules a redraw
+/
public void redrawSchedule() {
}
/++
Triggers a redraw
+/
public void redrawNow() {
}
}
+/
///
struct LoopCtrl {
int interval; /// in milliseconds
@ -890,7 +932,7 @@ final class PixmapPresenter {
_renderer = renderer;
// create software framebuffer
auto framebuffer = Pixmap(config.renderer.resolution);
auto framebuffer = Pixmap.makeNew(config.renderer.resolution);
// OpenGL?
auto openGlOptions = OpenGlOptions.no;
@ -911,6 +953,7 @@ final class PixmapPresenter {
);
window.windowResized = &this.windowResized;
window.cornerStyle = config.window.corners;
// alloc objects
_poc = new PresenterObjectsContainer(

514
pixmaprecorder.d Normal file
View File

@ -0,0 +1,514 @@
/+
== pixmaprecorder ==
Copyright Elias Batek (0xEAB) 2024.
Distributed under the Boost Software License, Version 1.0.
+/
/++
$(B Pixmap Recorder) is an auxiliary library for rendering video files from
[arsd.pixmappaint.Pixmap|Pixmap] frames by piping them to
[FFmpeg](https://ffmpeg.org/about.html).
$(SIDEBAR
Piping frame data into an independent copy of FFmpeg
enables this library to be used with a wide range of versions of said
third-party program
and (hopefully) helps to reduce the potential for breaking changes.
It also allows end-users to upgrade their possibilities by swapping the
accompanying copy FFmpeg.
This could be useful in cases where software distributors can only
provide limited functionality in their bundled binaries because of
legal requirements like patent licenses.
Keep in mind, support for more formats can be added to FFmpeg by
linking it against external libraries; such can also come with
additional distribution requirements that must be considered.
These things might be perceived as extra burdens and can make their
inclusion a matter of viability for distributors.
)
### Tips and tricks
$(TIP
The FFmpeg binary to be used can be specified by the optional
constructor parameter `ffmpegExecutablePath`.
It defaults to `ffmpeg`; this will trigger the usual lookup procedures
of the system the application runs on.
On POSIX this usually means searching for FFmpeg in the directories
specified by the environment variable PATH.
On Windows it will also look for an executable file with that name in
the current working directory.
)
$(TIP
The value of the `outputFormat` parameter of various constructor
overloads is passed to FFmpeg via the `-f` (format) option.
Run `ffmpeg -formats` to get a list of available formats.
)
$(TIP
To pass additional options to FFmpeg, use the
[PixmapRecorder.advancedFFmpegAdditionalOutputArgs|additional-output-args property].
)
$(TIP
Combining this module with [arsd.pixmappresenter|Pixmap Presenter]
is really straightforward.
In the most simplistic case, set up a [PixmapRecorder] before running
the presenter.
Then call
[PixmapRecorder.put|pixmapRecorder.record(presenter.framebuffer)]
at the end of the drawing callback in the eventloop.
---
auto recorder = new PixmapRecorder(60, /* … */);
scope(exit) {
const recorderStatus = recorder.stopRecording();
}
return presenter.eventLoop(delegate() {
// […]
recorder.record(presenter.framebuffer);
return LoopCtrl.redrawIn(16);
});
---
)
$(TIP
To use this module with [arsd.color] (which includes the image file
loading functionality provided by other arsd modules),
convert the
[arsd.color.TrueColorImage|TrueColorImage] or
[arsd.color.MemoryImage|MemoryImage] to a
[arsd.pixmappaint.Pixmap|Pixmap] first by calling
[arsd.pixmappaint.Pixmap.fromTrueColorImage|Pixmap.fromTrueColorImage()]
or
[arsd.pixmappaint.Pixmap.fromMemoryImage|Pixmap.fromMemoryImage()]
respectively.
)
### Examples
#### Getting started
$(NUMBERED_LIST
* Install FFmpeg (the CLI version).
$(LIST
* Debian derivatives (with FFmpeg in their repos): `apt install ffmpeg`
* Homebew: `brew install ffmpeg`
* Chocolatey: `choco install ffmpeg`
* Links to pre-built binaries can be found on <https://ffmpeg.org/download.html>.
)
* Determine where youve installed FFmpeg to.
Ideally, its somewhere within PATH so it can be run from the
command-line by just doing `ffmpeg`.
Otherwise, youll need the specific path to the executable to pass it
to the constructor of [PixmapRecorder].
)
---
import arsd.pixmaprecorder;
import arsd.pixmappaint;
/++
This demo renders a 1280×720 video at 30 FPS
fading from white (#FFF) to blue (#00F).
+/
int main() {
// Instantiate a recorder.
auto recorder = new PixmapRecorder(
30, // Video framerate [=FPS]
"out.mkv", // Output path to write the video file to.
);
// We will use this framebuffer later on to provide image data
// to the encoder.
auto frame = Pixmap(1280, 720);
for (int light = 0xFF; light >= 0; --light) {
auto color = Color(light, light, 0xFF);
frame.clear(color);
// Record the current frame.
// The video resolution to use is derived from the first frame.
recorder.put(frame);
}
// End and finalize the recording process.
return recorder.stopRecording();
}
---
+/
module arsd.pixmaprecorder;
import arsd.pixmappaint;
import std.format;
import std.path : buildPath;
import std.process;
import std.range : isOutputRange, OutputRange;
import std.sumtype;
import std.stdio : File;
private @safe {
auto stderrFauxSafe() @trusted {
import std.stdio : stderr;
return stderr;
}
auto stderr() {
return stderrFauxSafe;
}
alias RecorderOutput = SumType!(string, File);
}
/++
Video file encoder
Feed in video data frame by frame to encode video files
in one of the various formats supported by FFmpeg.
This is a convenience wrapper for piping pixmaps into FFmpeg.
FFmpeg will render an actual video file from the frame data.
This uses the CLI version of FFmpeg, no linking is required.
+/
final class PixmapRecorder : OutputRange!(const(Pixmap)) {
private {
string _ffmpegExecutablePath;
double _frameRate;
string _outputFormat;
RecorderOutput _output;
File _log;
string[] _outputAdditionalArgs;
Pid _pid;
Pipe _input;
Size _resolution;
bool _outputIsOurs = false;
}
@safe:
private this(
string ffmpegExecutablePath,
double frameRate,
string outputFormat,
RecorderOutput output,
File log,
) {
_ffmpegExecutablePath = ffmpegExecutablePath;
_frameRate = frameRate;
_outputFormat = outputFormat;
_output = output;
_log = log;
}
/++
Prepares a recorder for encoding a video file into the provided pipe.
$(WARNING
FFmpeg cannot produce certain formats in pipes.
Look out for error messages such as:
$(BLOCKQUOTE
`[mp4 @ 0xdead1337beef] muxer does not support non-seekable output`
)
This is not a limitation of this library (but rather one of FFmpeg).
Nevertheless, its still possible to use the affected formats.
Let FFmpeg output the video to the file path instead;
check out the other constructor overloads.
)
Params:
frameRate = Framerate of the video output; in frames per second.
output = File handle to write the video output to.
outputFormat = Video (container) format to output.
This value is passed to FFmpeg via the `-f` option.
log = Target file for the stderr log output of FFmpeg.
This is where error messages are written to.
ffmpegExecutablePath = Path to the FFmpeg executable
(e.g. `ffmpeg`, `ffmpeg.exe` or `/usr/bin/ffmpeg`).
$(COMMENT Keep this table in sync with the ones of other overloads.)
+/
public this(
double frameRate,
File output,
string outputFormat,
File log = stderr,
string ffmpegExecutablePath = "ffmpeg",
)
in (frameRate > 0)
in (output.isOpen)
in (outputFormat != "")
in (log.isOpen)
in (ffmpegExecutablePath != "") {
this(
ffmpegExecutablePath,
frameRate,
outputFormat,
RecorderOutput(output),
log,
);
}
/++
Prepares a recorder for encoding a video file
saved to the specified path.
$(TIP
This allows FFmpeg to seek through the output file
and enables the creation of file formats otherwise not supported
when using piped output.
)
Params:
frameRate = Framerate of the video output; in frames per second.
outputPath = File path to write the video output to.
Existing files will be overwritten.
FFmpeg will use this to autodetect the format
when no `outputFormat` is provided.
log = Target file for the stderr log output of FFmpeg.
This is where error messages are written to, as well.
outputFormat = Video (container) format to output.
This value is passed to FFmpeg via the `-f` option.
If `null`, the format is not provided and FFmpeg
will try to autodetect the format from the filename
of the `outputPath`.
ffmpegExecutablePath = Path to the FFmpeg executable
(e.g. `ffmpeg`, `ffmpeg.exe` or `/usr/bin/ffmpeg`).
$(COMMENT Keep this table in sync with the ones of other overloads.)
+/
public this(
double frameRate,
string outputPath,
File log = stderr,
string outputFormat = null,
string ffmpegExecutablePath = "ffmpeg",
)
in (frameRate > 0)
in ((outputPath != "") && (outputPath != "-"))
in (log.isOpen)
in ((outputFormat is null) || outputFormat != "")
in (ffmpegExecutablePath != "") {
// Sanitize the output path
// if it were to get confused with a command-line arg.
// Otherwise a relative path like `-my.mkv` would make FFmpeg complain
// about an “Unrecognized option 'out.mkv'”.
if (outputPath[0] == '-') {
outputPath = buildPath(".", outputPath);
}
this(
ffmpegExecutablePath,
frameRate,
null,
RecorderOutput(outputPath),
log,
);
}
/++
$(I Advanced users only:)
Additional command-line arguments to be passed to FFmpeg.
$(WARNING
The values provided through this property function are not
validated and passed verbatim to FFmpeg.
)
$(PITFALL
If code makes use of this and FFmpeg errors,
check the arguments provided here first.
)
+/
void advancedFFmpegAdditionalOutputArgs(string[] args) {
_outputAdditionalArgs = args;
}
/++
Determines whether the recorder is active
(which implies that an output file is open).
+/
bool isOpen() {
return _input.writeEnd.isOpen;
}
/// ditto
alias isRecording = isOpen;
private string[] buildFFmpegCommand() pure {
// Build resolution as understood by FFmpeg.
const string resolutionString = format!"%sx%s"(
_resolution.width,
_resolution.height,
);
// Convert framerate to string.
const string frameRateString = format!"%s"(_frameRate);
// Build command-line argument list.
auto cmd = [
_ffmpegExecutablePath,
"-y",
"-r",
frameRateString,
"-f",
"rawvideo",
"-pix_fmt",
"rgba",
"-s",
resolutionString,
"-i",
"-",
];
if (_outputFormat !is null) {
cmd ~= "-f";
cmd ~= _outputFormat;
}
if (_outputAdditionalArgs.length > 0) {
cmd = cmd ~ _outputAdditionalArgs;
}
cmd ~= _output.match!(
(string filePath) => filePath,
(ref File file) => "-",
);
return cmd;
}
/++
Starts the video encoding process.
Launches FFmpeg.
This function sets the video resolution for the encoding process.
All frames to record must match it.
$(SIDEBAR
Variable/dynamic resolution is neither supported by this library
nor by most real-world applications.
)
$(NOTE
This function is called by [put|put()] automatically.
Theres usually no need to call this manually.
)
+/
void open(const Size resolution)
in (!this.isOpen) {
// Save resolution for sanity checks.
_resolution = resolution;
const string[] cmd = buildFFmpegCommand();
// Prepare arsd → FFmpeg I/O pipe.
_input = pipe();
// Launch FFmpeg.
const processConfig = (
Config.suppressConsole
| Config.newEnv
);
// dfmt off
_pid = _output.match!(
delegate(string filePath) {
auto stdout = pipe();
stdout.readEnd.close();
return spawnProcess(
cmd,
_input.readEnd,
stdout.writeEnd,
_log,
null,
processConfig,
);
},
delegate(File file) {
auto stdout = pipe();
stdout.readEnd.close();
return spawnProcess(
cmd,
_input.readEnd,
file,
_log,
null,
processConfig,
);
}
);
// dfmt on
}
/// ditto
alias startRecording = close;
/++
Supplies the next frame to the video encoder.
$(TIP
This function automatically calls [open|open()] if necessary.
)
+/
void put(const Pixmap frame) @trusted {
if (!this.isOpen) {
this.open(frame.size);
} else {
assert(frame.size == _resolution, "Variable resolutions are not supported.");
}
_input.writeEnd.rawWrite(frame.data);
}
/// ditto
alias record = put;
/++
Ends the recording process.
$(NOTE
Waits for the FFmpeg process to exit in a blocking way.
)
Returns:
The status code provided by the FFmpeg program.
+/
int close() {
if (!this.isOpen) {
return 0;
}
_input.writeEnd.flush();
_input.writeEnd.close();
scope (exit) {
_input.close();
}
return wait(_pid);
}
/// ditto
alias stopRecording = close;
}
// self-test
private {
static assert(isOutputRange!(PixmapRecorder, Pixmap));
static assert(isOutputRange!(PixmapRecorder, const(Pixmap)));
}

View File

@ -179,13 +179,21 @@ class PostgreSql : Database {
PGconn* conn;
}
private string toLowerFast(string s) {
import std.ascii : isUpper;
foreach (c; s)
if (c >= 0x80 || isUpper(c))
return toLower(s);
return s;
}
///
class PostgresResult : ResultSet {
// name for associative array to result index
int getFieldIndex(string field) {
if(mapping is null)
makeFieldMapping();
field = field.toLower;
field = field.toLowerFast;
if(field in mapping)
return mapping[field];
else throw new Exception("no mapping " ~ field);

93
pptx.d Normal file
View File

@ -0,0 +1,93 @@
/++
Bare minimum support for reading Microsoft PowerPoint files.
History:
Added February 19, 2025
+/
module arsd.pptx;
// see ~/zip/ppt
import arsd.core;
import arsd.zip;
import arsd.dom;
import arsd.color;
/++
+/
class PptxFile {
private ZipFile zipFile;
private XmlDocument document;
/++
+/
this(FilePath file) {
this.zipFile = new ZipFile(file);
load();
}
/// ditto
this(immutable(ubyte)[] rawData) {
this.zipFile = new ZipFile(rawData);
load();
}
/// public for now but idk forever.
PptxSlide[] slides;
private string[string] contentTypes;
private struct Relationship {
string id;
string type;
string target;
}
private Relationship[string] relationships;
private void load() {
loadXml("[Content_Types].xml", (document) {
foreach(element; document.querySelectorAll("Override"))
contentTypes[element.attrs.PartName] = element.attrs.ContentType;
});
loadXml("ppt/_rels/presentation.xml.rels", (document) {
foreach(element; document.querySelectorAll("Relationship"))
relationships[element.attrs.Id] = Relationship(element.attrs.Id, element.attrs.Type, element.attrs.Target);
});
loadXml("ppt/presentation.xml", (document) {
this.document = document;
foreach(element; document.querySelectorAll("p\\:sldIdLst p\\:sldId"))
loadXml("ppt/" ~ relationships[element.getAttribute("r:id")].target, (document) {
slides ~= new PptxSlide(this, document);
});
});
// then there's slide masters and layouts and idk what that is yet
}
private void loadXml(string filename, scope void delegate(XmlDocument document) handler) {
auto document = new XmlDocument(cast(string) zipFile.getContent(filename));
handler(document);
}
}
class PptxSlide {
private PptxFile file;
private XmlDocument document;
private this(PptxFile file, XmlDocument document) {
this.file = file;
this.document = document;
}
/++
+/
string toPlainText() {
// FIXME: need to handle at least some of the layout
return document.root.innerText;
}
}

402
rtf.d Normal file
View File

@ -0,0 +1,402 @@
/++
Some support for the RTF file format - rich text format, like produced by Windows WordPad.
History:
Added February 13, 2025
+/
module arsd.rtf;
// https://www.biblioscape.com/rtf15_spec.htm
// https://latex2rtf.sourceforge.net/rtfspec_62.html
// https://en.wikipedia.org/wiki/Rich_Text_Format
// spacing is in "twips" or 1/20 of a point (as in text size unit). aka 1/1440th of an inch.
import arsd.core;
import arsd.color;
/++
+/
struct RtfDocument {
RtfGroup root;
/++
There are two helper functions to process a RTF file: one that does minimal processing
and sends you the data as it appears in the file, and one that sends you preprocessed
results upon significant state changes.
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.
+/
void process(void delegate(RtfPiece piece, ref RtfState state) dg) {
recurseIntoGroup(root, RtfState.init, dg);
}
private static void recurseIntoGroup(RtfGroup group, RtfState parentState, void delegate(RtfPiece piece, ref RtfState state) dg) {
// might need to copy...
RtfState state = parentState;
auto newDestination = group.destination;
if(newDestination.length)
state.currentDestination = newDestination;
foreach(piece; group.pieces) {
if(piece.contains == RtfPiece.Contains.group) {
recurseIntoGroup(piece.group, state, dg);
} else {
dg(piece, state);
}
}
}
//Color[] colorTable;
//Object[] fontTable;
}
/// ditto
RtfDocument readRtfFromString(const(char)[] s) {
return readRtfFromBytes(cast(const(ubyte)[]) s);
}
/// ditto
RtfDocument readRtfFromBytes(const(ubyte)[] s) {
RtfDocument document;
if(s.length < 7)
throw new ArsdException!"not a RTF file"("too short");
if((cast(char[]) s[0..6]) != `{\rtf1`)
throw new ArsdException!"not a RTF file"("wrong magic number");
document.root = parseRtfGroup(s);
return document;
}
/// ditto
struct RtfState {
string currentDestination;
}
unittest {
auto document = readRtfFromString("{\\rtf1Hello\nWorld}");
//import std.file; auto document = readRtfFromString(readText("/home/me/test.rtf"));
document.process((piece, ref state) {
final switch(piece.contains) {
case RtfPiece.Contains.controlWord:
// writeln(state.currentDestination, ": ", piece.controlWord);
break;
case RtfPiece.Contains.text:
// writeln(state.currentDestination, ": ", piece.text);
break;
case RtfPiece.Contains.group:
assert(0);
}
});
// writeln(toPlainText(document));
}
/++
Returns a plan text string that represents the jist of the document's content.
+/
string toPlainText(RtfDocument document) {
string ret;
document.process((piece, ref state) {
if(state.currentDestination.length)
return;
final switch(piece.contains) {
case RtfPiece.Contains.controlWord:
if(piece.controlWord.letterSequence == "par")
ret ~= "\n\n";
else if(piece.controlWord.toDchar != dchar.init)
ret ~= piece.controlWord.toDchar;
break;
case RtfPiece.Contains.text:
ret ~= piece.text;
break;
case RtfPiece.Contains.group:
assert(0);
}
});
return ret;
}
private RtfGroup parseRtfGroup(ref const(ubyte)[] s) {
RtfGroup group;
assert(s[0] == '{');
s = s[1 .. $];
if(s.length == 0)
throw new ArsdException!"bad RTF file"("premature end after {");
while(s[0] != '}') {
group.pieces ~= parseRtfPiece(s);
if(s.length == 0)
throw new ArsdException!"bad RTF file"("premature end before {");
}
s = s[1 .. $];
return group;
}
private RtfPiece parseRtfPiece(ref const(ubyte)[] s) {
while(true)
switch(s[0]) {
case '\\':
return RtfPiece(parseRtfControlWord(s));
case '{':
return RtfPiece(parseRtfGroup(s));
case '\t':
s = s[1 .. $];
return RtfPiece(RtfControlWord.tab);
case '\r':
case '\n':
// skip irrelevant characters
s = s[1 .. $];
continue;
default:
return RtfPiece(parseRtfText(s));
}
}
private RtfControlWord parseRtfControlWord(ref const(ubyte)[] s) {
assert(s[0] == '\\');
s = s[1 .. $];
if(s.length == 0)
throw new ArsdException!"bad RTF file"("premature end after \\");
RtfControlWord ret;
size_t pos;
do {
pos++;
} while(pos < s.length && isAlpha(cast(char) s[pos]));
ret.letterSequence = (cast(const char[]) s)[0 .. pos].idup;
s = s[pos .. $];
if(isAlpha(ret.letterSequence[0])) {
if(s.length == 0)
throw new ArsdException!"bad RTF file"("premature end after control word");
int readNumber() {
if(s.length == 0)
throw new ArsdException!"bad RTF file"("premature end when reading number");
int count;
while(s[count] >= '0' && s[count] <= '9')
count++;
if(count == 0)
throw new ArsdException!"bad RTF file"("expected negative number, got something else");
auto buffer = cast(const(char)[]) s[0 .. count];
s = s[count .. $];
int accumulator;
foreach(ch; buffer) {
accumulator *= 10;
accumulator += ch - '0';
}
return accumulator;
}
if(s[0] == '-') {
ret.hadNumber = true;
s = s[1 .. $];
ret.number = - readNumber();
// negative number
} else if(s[0] >= '0' && s[0] <= '9') {
// non-negative number
ret.hadNumber = true;
ret.number = readNumber();
}
if(s[0] == ' ') {
ret.hadSpaceAtEnd = true;
s = s[1 .. $];
}
} else {
// it was a control symbol
if(ret.letterSequence == "\r" || ret.letterSequence == "\n")
ret.letterSequence = "par";
}
return ret;
}
private string parseRtfText(ref const(ubyte)[] s) {
size_t end = s.length;
foreach(idx, ch; s) {
if(ch == '\\' || ch == '{' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '}') {
end = idx;
break;
}
}
auto ret = s[0 .. end];
s = s[end .. $];
// FIXME: charset conversion?
return (cast(const char[]) ret).idup;
}
// \r and \n chars w/o a \\ before them are ignored. but \ at the end of al ine is a \par
// \t is read but you should use \tab generally
// when reading, ima translate the ascii tab to \tab control word
// and ignore
/++
A union of entities you can see while parsing a RTF file.
+/
struct RtfPiece {
/++
+/
Contains contains() {
return contains_;
}
/// ditto
enum Contains {
controlWord,
group,
text
}
this(RtfControlWord cw) {
this.controlWord_ = cw;
this.contains_ = Contains.controlWord;
}
this(RtfGroup g) {
this.group_ = g;
this.contains_ = Contains.group;
}
this(string s) {
this.text_ = s;
this.contains_ = Contains.text;
}
/++
+/
RtfControlWord controlWord() {
if(contains != Contains.controlWord)
throw ArsdException!"RtfPiece type mismatch"(contains);
return controlWord_;
}
/++
+/
RtfGroup group() {
if(contains != Contains.group)
throw ArsdException!"RtfPiece type mismatch"(contains);
return group_;
}
/++
+/
string text() {
if(contains != Contains.text)
throw ArsdException!"RtfPiece type mismatch"(contains);
return text_;
}
private Contains contains_;
private union {
RtfControlWord controlWord_;
RtfGroup group_;
string text_;
}
}
// a \word thing
/++
A control word directly from the RTF file format.
+/
struct RtfControlWord {
bool hadSpaceAtEnd;
bool hadNumber;
string letterSequence; // what the word is
int number;
bool isDestination() {
switch(letterSequence) {
case
"author", "comment", "subject", "title",
"buptim", "creatim", "printim", "revtim",
"doccomm",
"footer", "footerf", "footerl", "footerr",
"footnote",
"ftncn", "ftnsep", "ftnsepc",
"header", "headerf", "headerl", "headerr",
"info", "keywords", "operator",
"pict",
"private",
"rxe",
"stylesheet",
"tc",
"txe",
"xe":
return true;
case "colortbl":
return true;
case "fonttbl":
return true;
default: return false;
}
}
dchar toDchar() {
switch(letterSequence) {
case "{": return '{';
case "}": return '}';
case `\`: return '\\';
case "~": return '\&nbsp;';
case "tab": return '\t';
case "line": return '\n';
default: return dchar.init;
}
}
bool isTurnOn() {
return !hadNumber || number != 0;
}
// take no delimiters
bool isControlSymbol() {
// if true, the letterSequence is the symbol
return letterSequence.length && !isAlpha(letterSequence[0]);
}
// letterSequence == ~ is a non breaking space
static RtfControlWord tab() {
RtfControlWord w;
w.letterSequence = "tab";
return w;
}
}
private bool isAlpha(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}
// a { ... } thing
/++
A group directly from the RTF file.
+/
struct RtfGroup {
RtfPiece[] pieces;
string destination() {
return isStarred() ?
((pieces.length > 1 && pieces[1].contains == RtfPiece.Contains.controlWord) ? pieces[1].controlWord.letterSequence : null)
: ((pieces.length && pieces[0].contains == RtfPiece.Contains.controlWord && pieces[0].controlWord.isDestination) ? pieces[0].controlWord.letterSequence : null);
}
bool isStarred() {
return (pieces.length && pieces[0].contains == RtfPiece.Contains.controlWord && pieces[0].controlWord.letterSequence == "*");
}
}
/+
\pard = paragraph defaults
+/

View File

@ -1777,7 +1777,10 @@ final class AudioPcmOutThreadImplementation : Thread {
public void unsuspend() {
suspended_ = false;
suspendWanted = false;
event.set();
static if(__traits(hasMember, event, "setIfInitialized"))
event.setIfInitialized();
else
event.set();
}
/// ditto
@ -2052,7 +2055,7 @@ struct AudioInput {
}
/// First, set [receiveData], then call this.
void record() {
void record() @system /* FIXME https://issues.dlang.org/show_bug.cgi?id=24782 */ {
assert(receiveData !is null);
recording = true;
@ -2203,7 +2206,7 @@ struct AudioOutput {
shared(bool) playing = false; // considered to be volatile
/// Starts playing, loops until stop is called
void play() {
void play() @system /* FIXME https://issues.dlang.org/show_bug.cgi?id=24782 */ {
if(handle is null)
open();

File diff suppressed because it is too large Load Diff

View File

@ -559,7 +559,7 @@ enum ConsoleOutputType {
cellular = 1, /// or do you want access to the terminal screen as a grid of characters?
//truncatedCellular = 3, /// cellular, but instead of wrapping output to the next line automatically, it will truncate at the edges
minimalProcessing = 255, /// do the least possible work, skips most construction and desturction tasks. Only use if you know what you're doing here
minimalProcessing = 255, /// do the least possible work, skips most construction and destruction tasks, does not query terminal in any way in favor of making assumptions about it. Only use if you know what you're doing here
}
alias ConsoleOutputMode = ConsoleOutputType;
@ -710,16 +710,16 @@ struct Terminal {
version(Posix) {
private int fdOut;
private int fdIn;
private int[] delegate() getSizeOverride;
void delegate(in void[]) _writeDelegate; // used to override the unix write() system call, set it magically
}
private int[] delegate() getSizeOverride;
bool terminalInFamily(string[] terms...) {
version(Win32Console) if(UseWin32Console)
return false;
// we're not writing to a terminal at all!
if(!usingDirectEmulator)
if(!usingDirectEmulator && type != ConsoleOutputType.minimalProcessing)
if(!stdoutIsTerminal || !stdinIsTerminal)
return false;
@ -728,7 +728,7 @@ struct Terminal {
version(TerminalDirectToEmulator)
auto term = "xterm";
else
auto term = environment.get("TERM");
auto term = type == ConsoleOutputType.minimalProcessing ? "xterm" : environment.get("TERM");
foreach(t; terms)
if(indexOf(term, t) != -1)
@ -900,7 +900,7 @@ struct Terminal {
// Looks up a termcap item and tries to execute it. Returns false on failure
bool doTermcap(T...)(string key, T t) {
if(!usingDirectEmulator && !stdoutIsTerminal)
if(!usingDirectEmulator && type != ConsoleOutputType.minimalProcessing && !stdoutIsTerminal)
return false;
import std.conv;
@ -1041,6 +1041,7 @@ struct Terminal {
private bool tcapsRequested;
uint tcaps() const {
if(type != ConsoleOutputType.minimalProcessing)
if(!tcapsRequested) {
Terminal* mutable = cast(Terminal*) &this;
version(Posix)
@ -1453,7 +1454,7 @@ struct Terminal {
this.type = type;
if(type == ConsoleOutputType.minimalProcessing) {
readTermcap();
readTermcap("xterm");
_suppressDestruction = true;
return;
}
@ -1468,6 +1469,7 @@ struct Terminal {
goCellular();
}
if(type != ConsoleOutputType.minimalProcessing)
if(terminalInFamily("xterm", "rxvt", "screen", "tmux")) {
writeStringRaw("\033[22;0t"); // save window title on a stack (support seems spotty, but it doesn't hurt to have it)
}
@ -1475,7 +1477,7 @@ struct Terminal {
}
private void goCellular() {
if(!usingDirectEmulator && !Terminal.stdoutIsTerminal)
if(!usingDirectEmulator && !Terminal.stdoutIsTerminal && type != ConsoleOutputType.minimalProcessing)
throw new Exception("Cannot go to cellular mode with redirected output");
if(UseVtSequences) {
@ -1737,7 +1739,7 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
/// Changes the current color. See enum [Color] for the values and note colors can be [arsd.docs.general_concepts#bitmasks|bitwise-or] combined with [Bright].
void color(int foreground, int background, ForceOption force = ForceOption.automatic, bool reverseVideo = false) {
if(!usingDirectEmulator && !stdoutIsTerminal)
if(!usingDirectEmulator && !stdoutIsTerminal && type != ConsoleOutputType.minimalProcessing)
return;
if(force != ForceOption.neverSend) {
if(UseVtSequences) {
@ -1967,7 +1969,7 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
/// Returns the terminal to normal output colors
void reset() {
if(!usingDirectEmulator && stdoutIsTerminal) {
if(!usingDirectEmulator && stdoutIsTerminal && type != ConsoleOutputType.minimalProcessing) {
if(UseVtSequences)
writeStringRaw("\033[0m");
else version(Win32Console) if(UseWin32Console) {
@ -2200,7 +2202,10 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
}
private int[] getSizeInternal() {
if(!usingDirectEmulator && !stdoutIsTerminal)
if(getSizeOverride)
return getSizeOverride();
if(!usingDirectEmulator && !stdoutIsTerminal && type != ConsoleOutputType.minimalProcessing)
throw new Exception("unable to get size of non-terminal");
version(Windows) {
CONSOLE_SCREEN_BUFFER_INFO info;
@ -2213,11 +2218,9 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
return [cols, rows];
} else {
if(getSizeOverride is null) {
winsize w;
ioctl(0, TIOCGWINSZ, &w);
return [w.ws_col, w.ws_row];
} else return getSizeOverride();
winsize w;
ioctl(1, TIOCGWINSZ, &w);
return [w.ws_col, w.ws_row];
}
}
@ -2315,6 +2318,34 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
if(s.length == 0)
return;
if(type == ConsoleOutputType.minimalProcessing) {
// need to still try to track a little, even if we can't
// talk to the terminal in minimal processing mode
auto height = this.height;
foreach(dchar ch; s) {
switch(ch) {
case '\n':
_cursorX = 0;
_cursorY++;
break;
case '\t':
int diff = 8 - (_cursorX % 8);
if(diff == 0)
diff = 8;
_cursorX += diff;
break;
default:
_cursorX++;
}
if(_wrapAround && _cursorX > width) {
_cursorX = 0;
_cursorY++;
}
if(_cursorY == height)
_cursorY--;
}
}
version(TerminalDirectToEmulator) {
// this breaks up extremely long output a little as an aid to the
@ -2478,7 +2509,7 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
On November 7, 2023 (dub v11.3), this function started returning stdin.readln in the event that the instance is not connected to a terminal.
+/
string getline(string prompt = null, dchar echoChar = dchar.init, string prefilledData = null) {
if(!usingDirectEmulator)
if(!usingDirectEmulator && type != ConsoleOutputType.minimalProcessing)
if(!stdoutIsTerminal || !stdinIsTerminal) {
import std.stdio;
import std.string;
@ -2532,6 +2563,8 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
Added January 8, 2023
+/
void updateCursorPosition() {
if(type == ConsoleOutputType.minimalProcessing)
return;
auto terminal = &this;
terminal.flush();
@ -2560,7 +2593,7 @@ http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.as
}
}
private void updateCursorPosition_impl() {
if(!usingDirectEmulator)
if(!usingDirectEmulator && type != ConsoleOutputType.minimalProcessing)
if(!stdinIsTerminal || !stdoutIsTerminal)
throw new Exception("cannot update cursor position on non-terminal");
auto terminal = &this;
@ -2763,7 +2796,7 @@ struct RealTimeConsoleInput {
It was in Terminal briefly during an undocumented period, but it had to be moved here to have the context needed to send the real time paste event.
+/
void requestPasteFromClipboard() {
void requestPasteFromClipboard() @system {
version(Win32Console) {
HWND hwndOwner = null;
if(OpenClipboard(hwndOwner) == 0)

View File

@ -1,8 +1,25 @@
/**
/++
This is an extendible unix terminal emulator and some helper functions to help actually implement one.
You'll have to subclass TerminalEmulator and implement the abstract functions as well as write a drawing function for it.
See minigui_addons/terminal_emulator_widget in arsd repo or nestedterminalemulator.d or main.d in my terminal-emulator repo for how I did it.
History:
Written September/October 2013ish. Moved to arsd 2020-03-26.
+/
module arsd.terminalemulator;
/+
FIXME
terminal optimization:
first invalidated + last invalidated to slice the array
when looking for things that need redrawing.
FIXME: writing a line in color then a line in ordinary does something
wrong.
# huh if i do underline then change color it undoes the underline
huh if i do underline then change color it undoes the underline
FIXME: make shift+enter send something special to the application
and shift+space, etc.
@ -19,19 +36,7 @@
FIXME: the save stack stuff should do cursor style too
This is an extendible unix terminal emulator and some helper functions to help actually implement one.
You'll have to subclass TerminalEmulator and implement the abstract functions as well as write a drawing function for it.
See nestedterminalemulator.d or main.d for how I did it.
*/
module arsd.terminalemulator;
/+
FIXME
terminal optimization:
first invalidated + last invalidated to slice the array
when looking for things that need redrawing.
+/
import arsd.color;
@ -3539,7 +3544,7 @@ version(use_libssh2) {
throw new Exception("fingerprint");
import std.string : toStringz;
if(auto err = libssh2_userauth_publickey_fromfile_ex(session, username.ptr, username.length, toStringz(keyFile ~ ".pub"), toStringz(keyFile), null))
if(auto err = libssh2_userauth_publickey_fromfile_ex(session, username.ptr, cast(int) username.length, toStringz(keyFile ~ ".pub"), toStringz(keyFile), null))
throw new Exception("auth");

View File

@ -24,6 +24,33 @@
+/
module arsd.textlayouter;
// FIXME: elastic tabstops https://nick-gravgaard.com/elastic-tabstops/
/+
Each cell ends with a tab character. A column block is a run of uninterrupted vertically adjacent cells. A column block is as wide as the widest piece of text in the cells it contains or a minimum width (plus padding). Text outside column blocks is ignored.
+/
// opening tabs work as indentation just like they do now, but wrt the algorithm are just considered one unit.
// then groups of lines with more tabs than the opening ones are processed together but only if they all right next to each other
// FIXME: soft word wrap w/ indentation preserved
// FIXME: line number stuff?
// want to support PS (new paragraph), LS (forced line break), FF (next page)
// and GS = <table> RS = <tr> US = <td> FS = </table> maybe.
// use \a bell for bookmarks in the text?
// note: ctrl+c == ascii 3 and ctrl+d == ascii 4 == end of text
// FIXME: maybe i need another overlay of block style not just text style. list, alignment, heading, paragraph spacing, etc. should it nest?
// FIXME: copy/paste preserving style.
// see: https://harfbuzz.github.io/a-simple-shaping-example.html
// FIXME: unicode private use area could be delegated out but it might also be used by something else.
// just really want an encoding scheme for replaced elements that punt it outside..
import arsd.simpledisplay;
/+
@ -106,6 +133,8 @@ import arsd.simpledisplay;
// You can do the caret by any time it gets drawn, you set the flag that it is on, then you can xor it to turn it off and keep track of that at top level.
// FIXME: might want to be able to swap out all styles at once and trigger whole relayout, as if a document theme changed wholesale, without changing the saved style handles
// FIXME: line and paragrpah numbering options while drawing
/++
Represents the style of a span of text.
@ -119,6 +148,39 @@ interface TextStyle {
+/
MeasurableFont font();
/++
History:
Added February 24, 2025
+/
//ParagraphMetrics paragraphMetrics();
// FIXME: list styles?
// FIXME: table styles?
/// ditto
static struct ParagraphMetrics {
/++
Extra spacing between each line, given in physical pixels.
+/
int lineSpacing;
/++
Spacing between each paragraph, given in physical pixels.
+/
int paragraphSpacing;
/++
Extra indentation on the first line of each paragraph, given in physical pixels.
+/
int paragraphIndentation;
// margin left and right?
/++
Note that TextAlignment.Left might be redefined to mean "Forward", meaning left if left-to-right, right if right-to-left,
but right now it ignores bidi anyway.
+/
TextAlignment alignment = TextAlignment.Left;
}
// FIXME: I might also want a duplicate function for saving state.
// verticalAlign?
@ -143,6 +205,13 @@ interface TextStyle {
return TerminalFontRepresentation.instance;
}
/++
The default returns reasonable values, you might want to call this to get the defaults,
then change some values and return the rest.
+/
ParagraphMetrics paragraphMetrics() {
return ParagraphMetrics.init;
}
}
}
@ -344,6 +413,15 @@ public struct Selection {
return this;
}
/++
Gets the current user coordinate, the point where they explicitly want the caret to be near.
History:
Added January 24, 2025
+/
Point getUserCoordinate() {
return impl.virtualFocusPosition;
}
/+ Moving the internal position +/
@ -1173,8 +1251,11 @@ class TextLayouter {
int length;
int styleInformationIndex;
bool isSpecialStyle;
}
/+
void resetSelection(int selectionId) {
}
@ -1188,6 +1269,7 @@ class TextLayouter {
void duplicateSelection(int receivingSelectionId, int sourceSelectionId) {
}
+/
private int findContainingSegment(int textOffset) {
@ -1212,6 +1294,8 @@ class TextLayouter {
Starts from the given selection and moves in the direction to find next.
Returns true if found.
NOT IMPLEMENTED use a selection instead
+/
FindResult find(int selectionId, in const(char)[] text, bool direction, bool wraparound) {
return FindResult.NotFound;
@ -1255,9 +1339,11 @@ class TextLayouter {
/++
Appends text at the end, without disturbing user selection.
Appends text at the end, without disturbing user selection. If style is not specified, it reuses the most recent style. If it is, it switches to that style.
If you put `isSpecialStyle` to `true`, the style will only apply to this text specifically and user edits will not inherit it.
+/
public void appendText(scope const(char)[] text, StyleHandle style = StyleHandle.init) {
public void appendText(scope const(char)[] text, StyleHandle style = StyleHandle.init, bool isSpecialStyle = false) {
wasMutated_ = true;
auto before = this.text;
this.text.length += text.length;
@ -1271,8 +1357,15 @@ class TextLayouter {
// otherwise, insert a new block for it
styles[$-1].length -= 1; // it no longer covers the zero terminator
// but this does, hence the +1
styles ~= StyleBlock(cast(int) before.length - 1, cast(int) text.length + 1, style.index);
if(isSpecialStyle) {
auto oldIndex = styles[$-1].styleInformationIndex;
styles ~= StyleBlock(cast(int) before.length - 1, cast(int) text.length, style.index, true);
// cover the zero terminator back in the old style
styles ~= StyleBlock(cast(int) this.text.length - 1, 1, oldIndex, false);
} else {
// but this does, hence the +1
styles ~= StyleBlock(cast(int) before.length - 1, cast(int) text.length + 1, style.index, false);
}
}
invalidateLayout(cast(int) before.length - 1 /* zero terminator */, this.text.length, cast(int) text.length);
@ -1284,6 +1377,8 @@ class TextLayouter {
FIXME: have a getTextInSelection
FIXME: have some kind of index stuff so you can select some text found in here (think regex search)
This function might be cut in a future version in favor of [getDrawableText]
+/
void getText(scope void delegate(scope const(char)[] segment, TextStyle style) handler) {
handler(text[0 .. $-1], null); // cut off the null terminator
@ -1300,6 +1395,8 @@ class TextLayouter {
return s;
}
alias getContentString = getTextString;
public static struct DrawingInformation {
Rectangle boundingBox;
Point initialBaseline;
@ -1341,11 +1438,13 @@ class TextLayouter {
return bb;
}
/+
void getTextAtPosition(Point p) {
relayoutIfNecessary();
// return the text in that segment, the style info attached, and if that specific point is part of a selection (can be used to tell if it should be a drag operation)
// then might want dropTextAt(Point p)
}
+/
/++
Gets the text that you need to draw, guaranteeing each call to your delegate will:
@ -1364,7 +1463,7 @@ class TextLayouter {
The segment may include all forms of whitespace, including newlines, tab characters, etc. Generally, a tab character will be in its own segment and \n will appear at the end of a segment. You will probably want to `stripRight` each segment depending on your drawing functions.
+/
void getDrawableText(scope bool delegate(scope const(char)[] segment, TextStyle style, DrawingInformation information, CaretInformation[] carets...) dg, Rectangle box = Rectangle.init) {
public void getDrawableText(scope bool delegate(scope const(char)[] segment, TextStyle style, DrawingInformation information, CaretInformation[] carets...) dg, Rectangle box = Rectangle.init) {
relayoutIfNecessary();
getInternalSegments(delegate bool(size_t segmentIndex, scope ref Segment segment) {
if(segment.textBeginOffset == -1)
@ -1485,7 +1584,7 @@ class TextLayouter {
// returns any segments that may lie inside the bounding box. if the box's size is 0, it is unbounded and goes through all segments
// may return more than is necessary; it uses the box as a hint to speed the search, not as the strict bounds it returns.
void getInternalSegments(scope bool delegate(size_t idx, scope ref Segment segment) dg, Rectangle box = Rectangle.init) {
protected void getInternalSegments(scope bool delegate(size_t idx, scope ref Segment segment) dg, Rectangle box = Rectangle.init) {
relayoutIfNecessary();
if(box.right == box.left)
@ -1571,6 +1670,7 @@ class TextLayouter {
return ts;
}
// most of these are unimplemented...
bool editable;
int wordWrapLength = 0;
int delegate(int x) tabStop = null;
@ -1655,12 +1755,14 @@ class TextLayouter {
user the result of this action.
+/
// FIXME: the public one might be like segmentOfClick so you can get the style info out (which might hold hyperlink data)
/+
Returns the nearest offset in the text for the given point.
it should return if it was inside the segment bounding box tho
might make this private
FIXME: the public one might be like segmentOfClick so you can get the style info out (which might hold hyperlink data)
+/
int offsetOfClick(Point p) {
int idx = cast(int) text.length - 1;
@ -1756,6 +1858,25 @@ class TextLayouter {
return idx;
}
/++
History:
Added September 13, 2024
+/
const(TextStyle) styleAtPoint(Point p) {
TextStyle s;
getInternalSegments(delegate bool(size_t segmentIndex, scope ref Segment segment) {
if(segment.boundingBox.contains(p)) {
s = stylePalette[segment.styleInformationIndex];
return false;
}
return true;
}, Rectangle(p, Size(1, 1)));
return s;
}
private StyleHandle getInsertionStyleAt(int offset) {
assert(offset >= 0 && offset < text.length);
/+
@ -1770,18 +1891,30 @@ class TextLayouter {
offset--; // use the previous one
}
return getStyleAt(offset);
return getStyleAt(offset, false);
}
private StyleHandle getStyleAt(int offset) {
private StyleHandle getStyleAt(int offset, bool allowSpecialStyle = true) {
// FIXME: binary search
foreach(style; styles) {
if(offset >= style.offset && offset < (style.offset + style.length))
foreach(idx, style; styles) {
if(offset >= style.offset && offset < (style.offset + style.length)) {
if(style.isSpecialStyle && !allowSpecialStyle) {
// we need to find the next style that is not special...
foreach(s2; styles[idx + 1 .. $])
if(!s2.isSpecialStyle)
return StyleHandle(s2.styleInformationIndex);
}
return StyleHandle(style.styleInformationIndex);
}
}
assert(0);
}
/++
Returns a bitmask of the selections active at any given offset.
May not be stable.
+/
ulong selectionsAt(int offset) {
ulong result;
ulong bit = 1;
@ -1808,7 +1941,7 @@ class TextLayouter {
private int justificationWidth_;
/++
Not implemented.
+/
public void justificationWidth(int width) {
if(width != justificationWidth_) {
@ -1817,12 +1950,32 @@ class TextLayouter {
}
}
/++
Can override this to define if a char is a word splitter for word wrapping.
+/
protected bool isWordwrapPoint(dchar c) {
// FIXME: assume private use characters are split points
if(c == ' ')
return true;
return false;
}
/+
/++
+/
protected ReplacedCharacter privateUseCharacterInfo(dchar c) {
return ReplacedCharacter.init;
}
/// ditto
static struct ReplacedCharacter {
bool overrideFont; /// if false, it uses the font like any other character, if true, it uses info from this struct
int width; /// in device pixels
int height; /// in device pixels
}
+/
private bool invalidateLayout_;
private int invalidStart = int.max;
private int invalidEnd = 0;
@ -1969,13 +2122,19 @@ class TextLayouter {
TextStyle currentStyle = null;
int currentStyleIndex = 0;
MeasurableFont font;
bool glyphCacheValid;
ubyte[128] glyphWidths;
void loadNewFont(MeasurableFont what) {
font = what;
// caching the ascii widths locally can give a boost to ~ 20% of the speed of this function
glyphCacheValid = true;
foreach(char c; 32 .. 128) {
auto w = font.stringWidth((&c)[0 .. 1]);
if(w >= 256) {
glyphCacheValid = false;
break;
}
glyphWidths[c] = cast(ubyte) w; // FIXME: what if it doesn't fit?
}
}
@ -2218,6 +2377,9 @@ class TextLayouter {
int thisWidth = 0;
// FIXME: delegate private-use area to their own segments
// FIXME: line separator, paragraph separator, form feed
switch(ch) {
case 0:
goto advance;
@ -2241,7 +2403,8 @@ class TextLayouter {
// a tab should be its own segment with no text
// per se
thisWidth = 48;
enum tabStop = 48;
thisWidth = 16 + tabStop - currentCorner.x % tabStop;
segment.width += thisWidth;
currentCorner.x += thisWidth;
@ -2264,7 +2427,7 @@ class TextLayouter {
thisWidth = width;
}
} else {
if(text[idx] < 128)
if(glyphCacheValid && text[idx] < 128)
thisWidth = glyphWidths[text[idx]];
else
thisWidth = font.stringWidth(text[idx .. idx + stride(text[idx])]);
@ -2297,10 +2460,17 @@ class TextLayouter {
}
}
finishLine(text.length, font);
auto finished = finishLine(text.length, font);
/+
if(!finished)
currentCorner.y += lineHeight;
import arsd.core; writeln(finished);
+/
_height = currentCorner.y;
// import arsd.core;writeln(_height);
assert(segments.length);
//return widths;

200
ttf.d
View File

@ -138,9 +138,10 @@ struct TtfFont {
// ~this() {}
}
/// Version of OpenGL you want it to use. Currently only one option.
/// Version of OpenGL you want it to use. Currently only two options.
enum OpenGlFontGLVersion {
old /// old style glBegin/glEnd stuff
old, /// old style glBegin/glEnd stuff
shader, /// newer style shader stuff
}
/+
@ -160,20 +161,17 @@ struct DrawingTextContext {
const int bottom; /// ditto
}
/++
Note that the constructor calls OpenGL functions and thus this must be called AFTER
the context creation, e.g. on simpledisplay's window first visible delegate.
abstract class OpenGlLimitedFontBase() {
void createShaders() {}
abstract uint glFormat();
abstract void startDrawing(Color color);
abstract void addQuad(
float s0, float t0, float x0, float y0,
float s1, float t1, float x1, float y1
);
abstract void finishDrawing();
Any text with characters outside the range you bake in the constructor are simply
ignored - that's why it is called "limited" font. The [TtfFont] struct can generate
any string on-demand which is more flexible, and even faster for strings repeated
frequently, but slower for frequently-changing or one-off strings. That's what this
class is made for.
History:
Added April 24, 2020
+/
class OpenGlLimitedFont(OpenGlFontGLVersion ver = OpenGlFontGLVersion.old) {
// FIXME: does this kern?
// FIXME: it would be cool if it did per-letter transforms too like word art. make it tangent to some baseline
@ -225,9 +223,7 @@ class OpenGlLimitedFont(OpenGlFontGLVersion ver = OpenGlFontGLVersion.old) {
bool actuallyDraw = color != Color.transparent;
if(actuallyDraw) {
glBindTexture(GL_TEXTURE_2D, _tex);
glColor4f(cast(float)color.r/255.0, cast(float)color.g/255.0, cast(float)color.b/255.0, cast(float)color.a / 255.0);
startDrawing(color);
}
bool newWord = true;
@ -330,19 +326,14 @@ class OpenGlLimitedFont(OpenGlFontGLVersion ver = OpenGlFontGLVersion.old) {
auto t1 = b.y1 * iph;
if(actuallyDraw) {
glBegin(GL_QUADS);
glTexCoord2f(s0, t0); glVertex2i(x0, y0);
glTexCoord2f(s1, t0); glVertex2i(x1, y0);
glTexCoord2f(s1, t1); glVertex2i(x1, y1);
glTexCoord2f(s0, t1); glVertex2i(x0, y1);
glEnd();
addQuad(s0, t0, x0, y0, s1, t1, x1, y1);
}
context.x += b.xadvance;
}
if(actuallyDraw)
glBindTexture(GL_TEXTURE_2D, 0); // unbind the texture
finishDrawing();
}
private {
@ -414,27 +405,180 @@ class OpenGlLimitedFont(OpenGlFontGLVersion ver = OpenGlFontGLVersion.old) {
this.descent = descent;
this.line_gap = line_gap;
assert(openGLCurrentContext() !is null);
glGenTextures(1, &_tex);
glBindTexture(GL_TEXTURE_2D, _tex);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_ALPHA,
glFormat,
width,
height,
0,
GL_ALPHA,
glFormat,
GL_UNSIGNED_BYTE,
buffer.ptr);
assert(!glGetError());
checkGlError();
glBindTexture(GL_TEXTURE_2D, 0);
createShaders();
}
}
/++
Note that the constructor calls OpenGL functions and thus this must be called AFTER
the context creation, e.g. on simpledisplay's window first visible delegate.
Any text with characters outside the range you bake in the constructor are simply
ignored - that's why it is called "limited" font. The [TtfFont] struct can generate
any string on-demand which is more flexible, and even faster for strings repeated
frequently, but slower for frequently-changing or one-off strings. That's what this
class is made for.
History:
Added April 24, 2020
+/
final class OpenGlLimitedFont(OpenGlFontGLVersion ver = OpenGlFontGLVersion.old) : OpenGlLimitedFontBase!() {
import arsd.color;
import arsd.simpledisplay;
public this(const ubyte[] ttfData, float fontPixelHeight, dchar firstChar = 32, dchar lastChar = 127) {
super(ttfData, fontPixelHeight, firstChar, lastChar);
}
override uint glFormat() {
// on new-style opengl this is the required value
static if(ver == OpenGlFontGLVersion.shader)
return GL_RED;
else
return GL_ALPHA;
}
static if(ver == OpenGlFontGLVersion.shader) {
private OpenGlShader shader;
private static struct Vertex {
this(float a, float b, float c, float d, float e = 0.0) {
t[0] = a;
t[1] = b;
xyz[0] = c;
xyz[1] = d;
xyz[2] = e;
}
float[2] t;
float[3] xyz;
}
private GlObject!Vertex glo;
// GL_DYNAMIC_DRAW FIXME
private Vertex[] vertixes;
private uint[] indexes;
public BasicMatrix!(4, 4) mymatrix;
public BasicMatrix!(4, 4) projection;
override void createShaders() {
mymatrix.loadIdentity();
projection.loadIdentity();
shader = new OpenGlShader(
OpenGlShader.Source(GL_VERTEX_SHADER, `
#version 330 core
`~glo.generateShaderDefinitions()~`
out vec2 texCoord;
uniform mat4 mymatrix;
uniform mat4 projection;
void main() {
gl_Position = projection * mymatrix * vec4(xyz, 1.0);
texCoord = t;
}
`),
OpenGlShader.Source(GL_FRAGMENT_SHADER, `
#version 330 core
in vec2 texCoord;
out vec4 FragColor;
uniform vec4 color;
uniform sampler2D tex;
void main() {
FragColor = color * vec4(1, 1, 1, texture(tex, texCoord).r);
}
`),
);
}
}
override void startDrawing(Color color) {
glBindTexture(GL_TEXTURE_2D, _tex);
static if(ver == OpenGlFontGLVersion.shader) {
shader.use();
shader.uniforms.color() = OGL.vec4f(cast(float)color.r/255.0, cast(float)color.g/255.0, cast(float)color.b/255.0, cast(float)color.a / 255.0);
shader.uniforms.mymatrix() = OGL.mat4x4f(mymatrix.data);
shader.uniforms.projection() = OGL.mat4x4f(projection.data);
} else {
glColor4f(cast(float)color.r/255.0, cast(float)color.g/255.0, cast(float)color.b/255.0, cast(float)color.a / 255.0);
}
}
override void addQuad(
float s0, float t0, float x0, float y0,
float s1, float t1, float x1, float y1
) {
static if(ver == OpenGlFontGLVersion.shader) {
auto idx = cast(int) vertixes.length;
vertixes ~= [
Vertex(s0, t0, x0, y0),
Vertex(s1, t0, x1, y0),
Vertex(s1, t1, x1, y1),
Vertex(s0, t1, x0, y1),
];
indexes ~= [
idx + 0,
idx + 1,
idx + 3,
idx + 1,
idx + 2,
idx + 3,
];
} else {
glBegin(GL_QUADS);
glTexCoord2f(s0, t0); glVertex2f(x0, y0);
glTexCoord2f(s1, t0); glVertex2f(x1, y0);
glTexCoord2f(s1, t1); glVertex2f(x1, y1);
glTexCoord2f(s0, t1); glVertex2f(x0, y1);
glEnd();
}
}
override void finishDrawing() {
static if(ver == OpenGlFontGLVersion.shader) {
glo = new typeof(glo)(vertixes, indexes);
glo.draw();
vertixes = vertixes[0..0];
vertixes.assumeSafeAppend();
indexes = indexes[0..0];
indexes.assumeSafeAppend();
}
glBindTexture(GL_TEXTURE_2D, 0); // unbind the texture
}
}

View File

@ -128,12 +128,12 @@ public import arsd.jsvar : var;
+/
class WebTemplateRenderer {
private TemplateLoader loader;
private EmbeddedTagResult function(string content, string[string] attributes)[string] embeddedTagTranslators;
private EmbeddedTagResult function(string content, AttributesHolder attributes)[string] embeddedTagTranslators;
/++
+/
this(TemplateLoader loader = null, EmbeddedTagResult function(string content, string[string] attributes)[string] embeddedTagTranslators = null) {
this(TemplateLoader loader = null, EmbeddedTagResult function(string content, AttributesHolder attributes)[string] embeddedTagTranslators = null) {
if(loader is null)
loader = TemplateLoader.forDirectory("templates/");
this.loader = loader;

View File

@ -12741,14 +12741,14 @@ struct cef_focus_handler_t
/// component.
///
///
/// Called when the browser component is requesting focus. |source| indicates
cef_base_ref_counted_t base;
extern(System) void function (
cef_focus_handler_t* self,
cef_browser_t* browser,
int next) nothrow on_take_focus;
///
/// Called when the browser component is requesting focus. |source| indicates
/// where the focus request is originating from. Return false (0) to allow the
/// focus to be set or true (1) to cancel setting the focus.
///

1438
xlsx.d Normal file

File diff suppressed because it is too large Load Diff

52
zip.d Normal file
View File

@ -0,0 +1,52 @@
/++
DO NOT USE - ZERO STABILITY AT THIS TIME.
Support for reading (and later, writing) .zip files.
Currently a wrapper around phobos to change the interface for consistency
and compatibility with my other modules.
You're better off using Phobos [std.zip] for stability at this time.
History:
Added February 19, 2025
+/
module arsd.zip;
import arsd.core;
import std.zip;
// https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
/++
+/
class ZipFile {
ZipArchive phobos;
/++
+/
this(immutable(ubyte)[] fileData) {
phobos = new ZipArchive(cast(void[]) fileData);
}
/// ditto
this(FilePath filename) {
import std.file;
this(cast(immutable(ubyte)[]) std.file.read(filename.toString()));
}
/++
Unstable, avoid.
+/
immutable(ubyte)[] getContent(string filename, bool allowEmptyIfNotExist = false) {
if(filename !in phobos.directory) {
if(allowEmptyIfNotExist)
return null;
throw ArsdException!"Zip content not found"(filename);
}
return cast(immutable(ubyte)[]) phobos.expand(phobos.directory[filename]);
}
}