Merge branch 'easier-text'
This commit is contained in:
		
						commit
						3a605da0d5
					
				
					 25 changed files with 951 additions and 192 deletions
				
			
		
							
								
								
									
										246
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										246
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							|  | @ -84,12 +84,27 @@ dependencies = [ | |||
|  "wyz", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "block-buffer" | ||||
| version = "0.10.4" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" | ||||
| dependencies = [ | ||||
|  "generic-array", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "byteorder" | ||||
| version = "1.5.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "bytes" | ||||
| version = "1.7.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "bzip2" | ||||
| version = "0.4.4" | ||||
|  | @ -132,9 +147,9 @@ dependencies = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "cc" | ||||
| version = "1.1.18" | ||||
| version = "1.1.29" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" | ||||
| checksum = "58e804ac3194a48bb129643eb1d62fcc20d18c6b8c181704489353d13120bcd1" | ||||
| dependencies = [ | ||||
|  "jobserver", | ||||
|  "libc", | ||||
|  | @ -149,9 +164,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" | |||
| 
 | ||||
| [[package]] | ||||
| name = "clap" | ||||
| version = "4.5.17" | ||||
| version = "4.5.20" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" | ||||
| checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" | ||||
| dependencies = [ | ||||
|  "clap_builder", | ||||
|  "clap_derive", | ||||
|  | @ -159,9 +174,9 @@ dependencies = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "clap_builder" | ||||
| version = "4.5.17" | ||||
| version = "4.5.20" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" | ||||
| checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" | ||||
| dependencies = [ | ||||
|  "anstream", | ||||
|  "anstyle", | ||||
|  | @ -171,9 +186,9 @@ dependencies = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "clap_derive" | ||||
| version = "4.5.13" | ||||
| version = "4.5.18" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" | ||||
| checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" | ||||
| dependencies = [ | ||||
|  "heck 0.5.0", | ||||
|  "proc-macro2", | ||||
|  | @ -193,6 +208,15 @@ version = "1.0.2" | |||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "cpufeatures" | ||||
| version = "0.2.14" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" | ||||
| dependencies = [ | ||||
|  "libc", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "crc32fast" | ||||
| version = "1.4.2" | ||||
|  | @ -202,6 +226,16 @@ dependencies = [ | |||
|  "cfg-if", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "crypto-common" | ||||
| version = "0.1.6" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" | ||||
| dependencies = [ | ||||
|  "generic-array", | ||||
|  "typenum", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "csbindgen" | ||||
| version = "1.9.3" | ||||
|  | @ -212,6 +246,22 @@ dependencies = [ | |||
|  "syn", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "data-encoding" | ||||
| version = "2.6.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "digest" | ||||
| version = "0.10.7" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" | ||||
| dependencies = [ | ||||
|  "block-buffer", | ||||
|  "crypto-common", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "equivalent" | ||||
| version = "1.0.1" | ||||
|  | @ -236,20 +286,36 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" | |||
| 
 | ||||
| [[package]] | ||||
| name = "flate2" | ||||
| version = "1.0.33" | ||||
| version = "1.0.34" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" | ||||
| checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" | ||||
| dependencies = [ | ||||
|  "crc32fast", | ||||
|  "miniz_oxide", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "fnv" | ||||
| version = "1.0.7" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "funty" | ||||
| version = "2.0.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "generic-array" | ||||
| version = "0.14.7" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" | ||||
| dependencies = [ | ||||
|  "typenum", | ||||
|  "version_check", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "getrandom" | ||||
| version = "0.2.15" | ||||
|  | @ -263,9 +329,9 @@ dependencies = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "hashbrown" | ||||
| version = "0.14.5" | ||||
| version = "0.15.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" | ||||
| checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "heck" | ||||
|  | @ -280,10 +346,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
| checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "indexmap" | ||||
| version = "2.5.0" | ||||
| name = "http" | ||||
| version = "1.1.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" | ||||
| checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" | ||||
| dependencies = [ | ||||
|  "bytes", | ||||
|  "fnv", | ||||
|  "itoa", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "httparse" | ||||
| version = "1.9.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "indexmap" | ||||
| version = "2.6.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" | ||||
| dependencies = [ | ||||
|  "equivalent", | ||||
|  "hashbrown", | ||||
|  | @ -320,9 +403,9 @@ dependencies = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "libc" | ||||
| version = "0.2.158" | ||||
| version = "0.2.159" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" | ||||
| checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "linux-raw-sys" | ||||
|  | @ -353,15 +436,15 @@ dependencies = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "once_cell" | ||||
| version = "1.19.0" | ||||
| version = "1.20.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" | ||||
| checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "pkg-config" | ||||
| version = "0.3.30" | ||||
| version = "0.3.31" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" | ||||
| checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "ppv-lite86" | ||||
|  | @ -374,9 +457,9 @@ dependencies = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "proc-macro2" | ||||
| version = "1.0.86" | ||||
| version = "1.0.87" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" | ||||
| checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" | ||||
| dependencies = [ | ||||
|  "unicode-ident", | ||||
| ] | ||||
|  | @ -428,9 +511,9 @@ dependencies = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "regex" | ||||
| version = "1.10.6" | ||||
| version = "1.11.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" | ||||
| checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" | ||||
| dependencies = [ | ||||
|  "aho-corasick", | ||||
|  "memchr", | ||||
|  | @ -440,9 +523,9 @@ dependencies = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "regex-automata" | ||||
| version = "0.4.7" | ||||
| version = "0.4.8" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" | ||||
| checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" | ||||
| dependencies = [ | ||||
|  "aho-corasick", | ||||
|  "memchr", | ||||
|  | @ -451,9 +534,9 @@ dependencies = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "regex-syntax" | ||||
| version = "0.8.4" | ||||
| version = "0.8.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" | ||||
| checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "rust-lzma" | ||||
|  | @ -467,9 +550,9 @@ dependencies = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "rustix" | ||||
| version = "0.38.36" | ||||
| version = "0.38.37" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3f55e80d50763938498dd5ebb18647174e0c76dc38c5505294bb224624f30f36" | ||||
| checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" | ||||
| dependencies = [ | ||||
|  "bitflags", | ||||
|  "errno", | ||||
|  | @ -518,30 +601,32 @@ dependencies = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "serde_spanned" | ||||
| version = "0.6.7" | ||||
| version = "0.6.8" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" | ||||
| checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" | ||||
| dependencies = [ | ||||
|  "serde", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "servicepoint" | ||||
| version = "0.8.0" | ||||
| version = "0.9.0" | ||||
| dependencies = [ | ||||
|  "bitvec", | ||||
|  "bzip2", | ||||
|  "clap", | ||||
|  "flate2", | ||||
|  "log", | ||||
|  "once_cell", | ||||
|  "rand", | ||||
|  "rust-lzma", | ||||
|  "tungstenite", | ||||
|  "zstd", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "servicepoint_binding_c" | ||||
| version = "0.8.0" | ||||
| version = "0.9.0" | ||||
| dependencies = [ | ||||
|  "cbindgen", | ||||
|  "servicepoint", | ||||
|  | @ -549,13 +634,24 @@ dependencies = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "servicepoint_binding_cs" | ||||
| version = "0.8.0" | ||||
| version = "0.9.0" | ||||
| dependencies = [ | ||||
|  "csbindgen", | ||||
|  "servicepoint", | ||||
|  "servicepoint_binding_c", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "sha1" | ||||
| version = "0.10.6" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" | ||||
| dependencies = [ | ||||
|  "cfg-if", | ||||
|  "cpufeatures", | ||||
|  "digest", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "shlex" | ||||
| version = "1.3.0" | ||||
|  | @ -570,9 +666,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" | |||
| 
 | ||||
| [[package]] | ||||
| name = "syn" | ||||
| version = "2.0.77" | ||||
| version = "2.0.79" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" | ||||
| checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  | @ -587,9 +683,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" | |||
| 
 | ||||
| [[package]] | ||||
| name = "tempfile" | ||||
| version = "3.12.0" | ||||
| version = "3.13.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" | ||||
| checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" | ||||
| dependencies = [ | ||||
|  "cfg-if", | ||||
|  "fastrand", | ||||
|  | @ -598,6 +694,26 @@ dependencies = [ | |||
|  "windows-sys 0.59.0", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "thiserror" | ||||
| version = "1.0.64" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" | ||||
| dependencies = [ | ||||
|  "thiserror-impl", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "thiserror-impl" | ||||
| version = "1.0.64" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "toml" | ||||
| version = "0.8.19" | ||||
|  | @ -621,9 +737,9 @@ dependencies = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "toml_edit" | ||||
| version = "0.22.20" | ||||
| version = "0.22.22" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" | ||||
| checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" | ||||
| dependencies = [ | ||||
|  "indexmap", | ||||
|  "serde", | ||||
|  | @ -633,10 +749,40 @@ dependencies = [ | |||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "unicode-ident" | ||||
| version = "1.0.12" | ||||
| name = "tungstenite" | ||||
| version = "0.24.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" | ||||
| checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" | ||||
| dependencies = [ | ||||
|  "byteorder", | ||||
|  "bytes", | ||||
|  "data-encoding", | ||||
|  "http", | ||||
|  "httparse", | ||||
|  "log", | ||||
|  "rand", | ||||
|  "sha1", | ||||
|  "thiserror", | ||||
|  "utf-8", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "typenum" | ||||
| version = "1.17.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "unicode-ident" | ||||
| version = "1.0.13" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "utf-8" | ||||
| version = "0.7.6" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "utf8parse" | ||||
|  | @ -650,6 +796,12 @@ version = "0.2.15" | |||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "version_check" | ||||
| version = "0.9.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "wasi" | ||||
| version = "0.11.0+wasi-snapshot-preview1" | ||||
|  | @ -740,9 +892,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" | |||
| 
 | ||||
| [[package]] | ||||
| name = "winnow" | ||||
| version = "0.6.18" | ||||
| version = "0.6.20" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" | ||||
| checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" | ||||
| dependencies = [ | ||||
|  "memchr", | ||||
| ] | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ members = [ | |||
| ] | ||||
| 
 | ||||
| [workspace.package] | ||||
| version = "0.8.0" | ||||
| version = "0.9.0" | ||||
| 
 | ||||
| [workspace.lints.rust] | ||||
| missing-docs = "warn" | ||||
|  |  | |||
|  | @ -20,24 +20,36 @@ bzip2 = { version = "0.4", optional = true } | |||
| zstd = { version = "0.13", optional = true } | ||||
| rust-lzma = { version = "0.6.0", optional = true } | ||||
| rand = { version = "0.8", optional = true } | ||||
| tungstenite = { version = "0.24.0", optional = true } | ||||
| once_cell = { version = "1.20.2", optional = true } | ||||
| 
 | ||||
| [features] | ||||
| default = ["compression_lzma"] | ||||
| default = ["compression_lzma", "protocol_udp", "cp437"] | ||||
| compression_zlib = ["dep:flate2"] | ||||
| compression_bzip2 = ["dep:bzip2"] | ||||
| compression_lzma = ["dep:rust-lzma"] | ||||
| compression_zstd = ["dep:zstd"] | ||||
| all_compressions = ["compression_zlib", "compression_bzip2", "compression_lzma", "compression_zstd"] | ||||
| rand = ["dep:rand"] | ||||
| protocol_udp = [] | ||||
| protocol_websocket = ["dep:tungstenite"] | ||||
| cp437 = ["dep:once_cell"] | ||||
| 
 | ||||
| [[example]] | ||||
| name = "random_brightness" | ||||
| required-features = ["rand"] | ||||
| 
 | ||||
| [[example]] | ||||
| name = "game_of_life" | ||||
| required-features = ["rand"] | ||||
| 
 | ||||
| [[example]] | ||||
| name = "websocket" | ||||
| required-features = ["protocol_websocket"] | ||||
| 
 | ||||
| [dev-dependencies] | ||||
| # for examples | ||||
| clap = { version = "4.5", features = ["derive"] } | ||||
| rand = "0.8" | ||||
| 
 | ||||
| [lints] | ||||
| workspace = true | ||||
|  | @ -9,6 +9,17 @@ In [CCCB](https://berlin.ccc.de/), there is a big pixel matrix hanging on the wa | |||
| Display" or "Airport Display". | ||||
| This crate contains a library for parsing, encoding and sending packets to this display via UDP. | ||||
| 
 | ||||
| ## Installation | ||||
| 
 | ||||
| ```bash | ||||
| cargo add servicepoint | ||||
| ``` | ||||
| or | ||||
| ```toml | ||||
| [dependencies] | ||||
| servicepoint = "0.9.0" | ||||
| ``` | ||||
| 
 | ||||
| ## Examples | ||||
| 
 | ||||
| ```rust | ||||
|  | @ -23,7 +34,7 @@ fn main() { | |||
| } | ||||
| ``` | ||||
| 
 | ||||
| More examples are available in the crate.  | ||||
| More examples are available in the crate. | ||||
| Execute `cargo run --example` for a list of available examples and `cargo run --example <name>` to run one. | ||||
| 
 | ||||
| ## Note on stability | ||||
|  | @ -32,22 +43,21 @@ This library is still in early development. | |||
| You can absolutely use it, and it works, but expect minor breaking changes with every version bump. | ||||
| Please specify the full version including patch in your Cargo.toml until 1.0 is released. | ||||
| 
 | ||||
| ## Installation | ||||
| 
 | ||||
| ```bash | ||||
| cargo add servicepoint | ||||
| ``` | ||||
| 
 | ||||
| ## Features | ||||
| 
 | ||||
| This library has multiple compression libraries as optional dependencies. | ||||
| If you do not need compression/decompression support you can disable those features. | ||||
| In the likely case you only need one of them, you can include that one specifically. | ||||
| This library has multiple optional dependencies. | ||||
| You can choose to (not) include them by toggling the related features. | ||||
| 
 | ||||
| ```toml | ||||
| [dependencies] | ||||
| servicepoint = { version = "0.8.0", default-features = false, features = ["compression-bz"] } | ||||
| ``` | ||||
| | Name               | Default | Description                                | | ||||
| |--------------------|---------|--------------------------------------------| | ||||
| | compression_zlib   | false   | Enable additional compression algo         | | ||||
| | compression_bzip2  | false   | Enable additional compression algo         | | ||||
| | compression_lzma   | true    | Enable additional compression algo         | | ||||
| | compression_zstd   | false   | Enable additional compression algo         | | ||||
| | protocol_udp       | true    | Connection::Udp                            | | ||||
| | protocol_websocket | false   | Connection::WebSocket                      | | ||||
| | rand               | false   | impl Distribution<Brightness> for Standard | | ||||
| | cp437              | true    | Conversion to and from CP-437              | | ||||
| 
 | ||||
| ## Everything else | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| use clap::Parser; | ||||
| 
 | ||||
| use servicepoint::{Command, Connection, Cp437Grid, Grid, Origin}; | ||||
| use servicepoint::{CharGrid, Command, Connection, Cp437Grid, Origin}; | ||||
| 
 | ||||
| #[derive(Parser, Debug)] | ||||
| struct Cli { | ||||
|  | @ -31,19 +31,15 @@ fn main() { | |||
|             .expect("sending clear failed"); | ||||
|     } | ||||
| 
 | ||||
|     let max_width = cli.text.iter().map(|t| t.len()).max().unwrap(); | ||||
|     let text = cli.text.iter().fold(String::new(), move |str, line| { | ||||
|         let is_first = str.is_empty(); | ||||
|         str + if is_first { "" } else { "\n" } + line | ||||
|     }); | ||||
| 
 | ||||
|     let mut chars = Cp437Grid::new(max_width, cli.text.len()); | ||||
|     for y in 0..cli.text.len() { | ||||
|         let row = &cli.text[y]; | ||||
| 
 | ||||
|         for (x, char) in row.chars().enumerate() { | ||||
|             let char = char.try_into().expect("invalid input char"); | ||||
|             chars.set(x, y, char); | ||||
|         } | ||||
|     } | ||||
|     let grid = CharGrid::from(&*text); | ||||
|     let cp437_grid = Cp437Grid::from(&grid); | ||||
| 
 | ||||
|     connection | ||||
|         .send(Command::Cp437Data(Origin::new(0, 0), chars)) | ||||
|         .send(Command::Cp437Data(Origin::new(0, 0), cp437_grid)) | ||||
|         .expect("sending text failed"); | ||||
| } | ||||
|  |  | |||
							
								
								
									
										27
									
								
								crates/servicepoint/examples/websocket.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								crates/servicepoint/examples/websocket.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| //! Example for how to use the WebSocket connection
 | ||||
| 
 | ||||
| use servicepoint::{ | ||||
|     Command, CompressionCode, Connection, Grid, Origin, PixelGrid, | ||||
| }; | ||||
| 
 | ||||
| fn main() { | ||||
|     // make connection mut
 | ||||
|     let mut connection = | ||||
|         Connection::open_websocket("ws://localhost:8080".parse().unwrap()) | ||||
|             .unwrap(); | ||||
| 
 | ||||
|     // use send_mut instead of send
 | ||||
|     connection.send_mut(Command::Clear).unwrap(); | ||||
| 
 | ||||
|     let mut pixels = PixelGrid::max_sized(); | ||||
|     pixels.fill(true); | ||||
| 
 | ||||
|     // use send_mut instead of send
 | ||||
|     connection | ||||
|         .send_mut(Command::BitmapLinearWin( | ||||
|             Origin::ZERO, | ||||
|             pixels, | ||||
|             CompressionCode::Lzma, | ||||
|         )) | ||||
|         .unwrap(); | ||||
| } | ||||
|  | @ -77,8 +77,8 @@ impl From<BrightnessGrid> for Vec<u8> { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<BrightnessGrid> for PrimitiveGrid<u8> { | ||||
|     fn from(value: PrimitiveGrid<Brightness>) -> Self { | ||||
| impl From<&BrightnessGrid> for PrimitiveGrid<u8> { | ||||
|     fn from(value: &PrimitiveGrid<Brightness>) -> Self { | ||||
|         let u8s = value | ||||
|             .iter() | ||||
|             .map(|brightness| (*brightness).into()) | ||||
|  | @ -109,3 +109,33 @@ impl Distribution<Brightness> for Standard { | |||
|         Brightness(rng.gen_range(Brightness::MIN.0..=Brightness::MAX.0)) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
|     use crate::DataRef; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn brightness_from_u8() { | ||||
|         assert_eq!(Err(100), Brightness::try_from(100)); | ||||
|         assert_eq!(Ok(Brightness(1)), Brightness::try_from(1)) | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     #[cfg(feature = "rand")] | ||||
|     fn rand_brightness() { | ||||
|         let mut rng = rand::thread_rng(); | ||||
|         for _ in 0..100 { | ||||
|             let _: Brightness = rng.gen(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn to_u8_grid() { | ||||
|         let mut grid = BrightnessGrid::new(2, 2); | ||||
|         grid.set(1, 0, Brightness::MIN); | ||||
|         grid.set(0, 1, Brightness::MAX); | ||||
|         let actual = PrimitiveGrid::from(&grid); | ||||
|         assert_eq!(actual.data_ref(), &[11, 0, 11, 11]); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,30 +1,50 @@ | |||
| use bitvec::prelude::BitVec; | ||||
| 
 | ||||
| use crate::{ | ||||
|     command_code::CommandCode, compression::into_decompressed, Brightness, | ||||
|     BrightnessGrid, CompressionCode, Header, Origin, Packet, PixelGrid, Pixels, | ||||
|     PrimitiveGrid, SpBitVec, Tiles, TILE_SIZE, | ||||
|     command_code::CommandCode, | ||||
|     compression::into_decompressed, | ||||
|     packet::{Header, Packet}, | ||||
|     Brightness, BrightnessGrid, CompressionCode, Cp437Grid, Origin, PixelGrid, | ||||
|     Pixels, PrimitiveGrid, SpBitVec, Tiles, TILE_SIZE, | ||||
| }; | ||||
| 
 | ||||
| /// Type alias for documenting the meaning of the u16 in enum values
 | ||||
| pub type Offset = usize; | ||||
| 
 | ||||
| /// A grid containing codepage 437 characters.
 | ||||
| ///
 | ||||
| /// The encoding is currently not enforced.
 | ||||
| pub type Cp437Grid = PrimitiveGrid<u8>; | ||||
| 
 | ||||
| /// A low-level display command.
 | ||||
| ///
 | ||||
| /// This struct and associated functions implement the UDP protocol for the display.
 | ||||
| ///
 | ||||
| /// To send a `Command`, use a `Connection`.
 | ||||
| /// To send a [Command], use a [connection][crate::Connection].
 | ||||
| ///
 | ||||
| /// # Available commands
 | ||||
| ///
 | ||||
| /// To send text, take a look at [Command::Cp437Data].
 | ||||
| ///
 | ||||
| /// To draw pixels, the easiest command to use is [Command::BitmapLinearWin].
 | ||||
| ///
 | ||||
| /// The other BitmapLinear-Commands operate on a region of pixel memory directly.
 | ||||
| /// [Command::BitmapLinear] overwrites a region.
 | ||||
| /// [Command::BitmapLinearOr], [Command::BitmapLinearAnd] and [Command::BitmapLinearXor] apply logical operations per pixel.
 | ||||
| ///
 | ||||
| /// Out of bounds operations may be truncated or ignored by the display.
 | ||||
| ///
 | ||||
| /// # Compression
 | ||||
| ///
 | ||||
| /// Some commands can contain compressed payloads.
 | ||||
| /// To get started, use [CompressionCode::Uncompressed].
 | ||||
| ///
 | ||||
| /// If you want to archive the best performance (e.g. latency),
 | ||||
| /// you can try the different compression algorithms for your hardware and use case.
 | ||||
| ///
 | ||||
| /// In memory, the payload is not compressed in the [Command].
 | ||||
| /// Payload (de-)compression happens when converting the [Command] into a [Packet] or vice versa.
 | ||||
| ///
 | ||||
| /// # Examples
 | ||||
| ///
 | ||||
| /// ```rust
 | ||||
| /// # use servicepoint::{Brightness, Command, Connection, Packet};
 | ||||
| ///
 | ||||
| /// # use servicepoint::{Brightness, Command, Connection, packet::Packet};
 | ||||
| /// #
 | ||||
| /// // create command
 | ||||
| /// let command = Command::Brightness(Brightness::MAX);
 | ||||
| ///
 | ||||
|  | @ -56,6 +76,8 @@ pub enum Command { | |||
| 
 | ||||
|     /// Show text on the screen.
 | ||||
|     ///
 | ||||
|     /// The text is sent in the form of a 2D grid of characters.
 | ||||
|     ///
 | ||||
|     /// <div class="warning">
 | ||||
|     ///     The library does not currently convert between UTF-8 and CP-437.
 | ||||
|     ///     Because Rust expects UTF-8 strings, it might be necessary to only send ASCII for now.
 | ||||
|  | @ -64,15 +86,45 @@ pub enum Command { | |||
|     /// # Examples
 | ||||
|     ///
 | ||||
|     /// ```rust
 | ||||
|     /// # use servicepoint::{Command, Connection, Origin};
 | ||||
|     /// # let connection = Connection::Fake;
 | ||||
|     /// use servicepoint::{CharGrid, Cp437Grid};
 | ||||
|     /// let grid = CharGrid::from("Hello,\nWorld!");
 | ||||
|     /// let grid = Cp437Grid::from(&grid);
 | ||||
|     /// connection.send(Command::Cp437Data(Origin::ZERO, grid)).expect("send failed");
 | ||||
|     /// ```
 | ||||
|     ///
 | ||||
|     /// ```rust
 | ||||
|     /// # use servicepoint::{Command, Connection, Cp437Grid, Origin};
 | ||||
|     /// # let connection = Connection::open("127.0.0.1:2342").unwrap();
 | ||||
|     /// let chars = ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd'].map(move |c| c as u8);
 | ||||
|     /// let grid = Cp437Grid::load(5, 2, &chars);
 | ||||
|     /// # let connection = Connection::Fake;
 | ||||
|     /// let grid = Cp437Grid::load_ascii("Hello\nWorld", 5, false).unwrap();
 | ||||
|     /// connection.send(Command::Cp437Data(Origin::new(2, 2), grid)).unwrap();
 | ||||
|     /// ```
 | ||||
|     Cp437Data(Origin<Tiles>, Cp437Grid), | ||||
| 
 | ||||
|     /// Sets a window of pixels to the specified values
 | ||||
|     /// Overwrites a rectangular region of pixels.
 | ||||
|     ///
 | ||||
|     /// Origin coordinates must be divisible by 8.
 | ||||
|     ///
 | ||||
|     /// # Examples
 | ||||
|     ///
 | ||||
|     /// ```rust
 | ||||
|     /// # use servicepoint::{Command, CompressionCode, Grid, PixelGrid};
 | ||||
|     /// # let connection = servicepoint::Connection::Fake;
 | ||||
|     /// #
 | ||||
|     /// let mut pixels = PixelGrid::max_sized();
 | ||||
|     /// // draw something to the pixels here
 | ||||
|     /// # pixels.set(2, 5, true);
 | ||||
|     ///
 | ||||
|     /// // create command to send pixels
 | ||||
|     /// let command = Command::BitmapLinearWin(
 | ||||
|     ///    servicepoint::Origin::new(0, 0),
 | ||||
|     ///    pixels,
 | ||||
|     ///    CompressionCode::Uncompressed
 | ||||
|     /// );
 | ||||
|     ///
 | ||||
|     /// connection.send(command).expect("send failed");
 | ||||
|     /// ```
 | ||||
|     BitmapLinearWin(Origin<Pixels>, PixelGrid, CompressionCode), | ||||
| 
 | ||||
|     /// Set the brightness of all tiles to the same value.
 | ||||
|  | @ -95,7 +147,7 @@ pub enum Command { | |||
|     /// The screen will continuously overwrite more pixel data without regarding the offset, meaning
 | ||||
|     /// once the starting row is full, overwriting will continue on column 0.
 | ||||
|     ///
 | ||||
|     /// The contained `BitVec` is always uncompressed.
 | ||||
|     /// The contained [BitVec] is always uncompressed.
 | ||||
|     BitmapLinear(Offset, SpBitVec, CompressionCode), | ||||
| 
 | ||||
|     /// Set pixel data according to an and-mask starting at the offset.
 | ||||
|  | @ -103,7 +155,7 @@ pub enum Command { | |||
|     /// The screen will continuously overwrite more pixel data without regarding the offset, meaning
 | ||||
|     /// once the starting row is full, overwriting will continue on column 0.
 | ||||
|     ///
 | ||||
|     /// The contained `BitVec` is always uncompressed.
 | ||||
|     /// The contained [BitVec] is always uncompressed.
 | ||||
|     BitmapLinearAnd(Offset, SpBitVec, CompressionCode), | ||||
| 
 | ||||
|     /// Set pixel data according to an or-mask starting at the offset.
 | ||||
|  | @ -111,7 +163,7 @@ pub enum Command { | |||
|     /// The screen will continuously overwrite more pixel data without regarding the offset, meaning
 | ||||
|     /// once the starting row is full, overwriting will continue on column 0.
 | ||||
|     ///
 | ||||
|     /// The contained `BitVec` is always uncompressed.
 | ||||
|     /// The contained [BitVec] is always uncompressed.
 | ||||
|     BitmapLinearOr(Offset, SpBitVec, CompressionCode), | ||||
| 
 | ||||
|     /// Set pixel data according to a xor-mask starting at the offset.
 | ||||
|  | @ -119,7 +171,7 @@ pub enum Command { | |||
|     /// The screen will continuously overwrite more pixel data without regarding the offset, meaning
 | ||||
|     /// once the starting row is full, overwriting will continue on column 0.
 | ||||
|     ///
 | ||||
|     /// The contained `BitVec` is always uncompressed.
 | ||||
|     /// The contained [BitVec] is always uncompressed.
 | ||||
|     BitmapLinearXor(Offset, SpBitVec, CompressionCode), | ||||
| 
 | ||||
|     /// Kills the udp daemon on the display, which usually results in a restart.
 | ||||
|  | @ -166,7 +218,7 @@ pub enum Command { | |||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| /// Err values for `Command::try_from`.
 | ||||
| /// Err values for [Command::try_from].
 | ||||
| #[derive(PartialEq)] | ||||
| pub enum TryFromPacketError { | ||||
|     /// the contained command code does not correspond to a known command
 | ||||
|  | @ -188,7 +240,7 @@ pub enum TryFromPacketError { | |||
| impl TryFrom<Packet> for Command { | ||||
|     type Error = TryFromPacketError; | ||||
| 
 | ||||
|     /// Try to interpret the `Packet` as one containing a `Command`
 | ||||
|     /// Try to interpret the [Packet] as one containing a [Command]
 | ||||
|     fn try_from(packet: Packet) -> Result<Self, Self::Error> { | ||||
|         let Packet { | ||||
|             header: Header { | ||||
|  | @ -443,9 +495,12 @@ impl Command { | |||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use crate::{ | ||||
|         bitvec::prelude::BitVec, command::TryFromPacketError, | ||||
|         command_code::CommandCode, origin::Pixels, Brightness, Command, | ||||
|         CompressionCode, Header, Origin, Packet, PixelGrid, PrimitiveGrid, | ||||
|         bitvec::prelude::BitVec, | ||||
|         command::TryFromPacketError, | ||||
|         command_code::CommandCode, | ||||
|         origin::Pixels, | ||||
|         packet::{Header, Packet}, | ||||
|         Brightness, Command, CompressionCode, Origin, PixelGrid, PrimitiveGrid, | ||||
|     }; | ||||
| 
 | ||||
|     fn round_trip(original: Command) { | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| /// The u16 command codes used for the `Commands`.
 | ||||
| /// The u16 command codes used for the [Command]s.
 | ||||
| #[repr(u16)] | ||||
| #[derive(Debug, Copy, Clone)] | ||||
| pub(crate) enum CommandCode { | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ use flate2::{FlushCompress, FlushDecompress, Status}; | |||
| #[cfg(feature = "compression_zstd")] | ||||
| use zstd::{Decoder as ZstdDecoder, Encoder as ZstdEncoder}; | ||||
| 
 | ||||
| use crate::{CompressionCode, Payload}; | ||||
| use crate::{packet::Payload, CompressionCode}; | ||||
| 
 | ||||
| pub(crate) fn into_decompressed( | ||||
|     kind: CompressionCode, | ||||
|  |  | |||
|  | @ -1,29 +1,61 @@ | |||
| use std::fmt::Debug; | ||||
| use std::net::{ToSocketAddrs, UdpSocket}; | ||||
| 
 | ||||
| use log::{debug, info}; | ||||
| 
 | ||||
| use crate::Packet; | ||||
| use crate::packet::Packet; | ||||
| 
 | ||||
| /// A connection to the display.
 | ||||
| ///
 | ||||
| /// Used to send [Packets][Packet] or [Commands][crate::Command].
 | ||||
| ///
 | ||||
| /// # Examples
 | ||||
| /// ```rust
 | ||||
| /// let connection = servicepoint::Connection::open("172.23.42.29:2342")
 | ||||
| /// let connection = servicepoint::Connection::open("127.0.0.1:2342")
 | ||||
| ///     .expect("connection failed");
 | ||||
| ///  connection.send(servicepoint::Command::Clear)
 | ||||
| ///     .expect("send failed");
 | ||||
| /// ```
 | ||||
| #[derive(Debug)] | ||||
| pub enum Connection { | ||||
|     /// A real connection using the UDP protocol
 | ||||
|     Udp(UdpSocket), | ||||
|     /// A fake connection for testing that does not actually send anything
 | ||||
|     /// A connection using the UDP protocol.
 | ||||
|     ///
 | ||||
|     /// Use this when sending commands directly to the display.
 | ||||
|     ///
 | ||||
|     /// Requires the feature "protocol_udp" which is enabled by default.
 | ||||
|     #[cfg(feature = "protocol_udp")] | ||||
|     Udp(std::net::UdpSocket), | ||||
| 
 | ||||
|     /// A connection using the WebSocket protocol.
 | ||||
|     ///
 | ||||
|     /// Note that you will need to forward the WebSocket messages via UDP to the display.
 | ||||
|     /// You can use [servicepoint-websocket-relay] for this.
 | ||||
|     ///
 | ||||
|     /// To create a new WebSocket automatically, use [Connection::open_websocket].
 | ||||
|     ///
 | ||||
|     /// Requires the feature "protocol_websocket" which is disabled by default.
 | ||||
|     ///
 | ||||
|     /// [servicepoint-websocket-relay]: https://github.com/kaesaecracker/servicepoint-websocket-relay
 | ||||
|     #[cfg(feature = "protocol_websocket")] | ||||
|     WebSocket( | ||||
|         tungstenite::WebSocket< | ||||
|             tungstenite::stream::MaybeTlsStream<std::net::TcpStream>, | ||||
|         >, | ||||
|     ), | ||||
| 
 | ||||
|     /// A fake connection for testing that does not actually send anything.
 | ||||
|     ///
 | ||||
|     /// This variant allows immutable send.
 | ||||
|     Fake, | ||||
| 
 | ||||
|     /// A fake connection for testing that does not actually send anything.
 | ||||
|     ///
 | ||||
|     /// This variant does not allow immutable send.
 | ||||
|     FakeMutableSend, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub enum SendError { | ||||
|     IoError(std::io::Error), | ||||
|     #[cfg(feature = "protocol_websocket")] | ||||
|     WebsocketError(tungstenite::Error), | ||||
| } | ||||
| 
 | ||||
| impl Connection { | ||||
|  | @ -31,24 +63,110 @@ impl Connection { | |||
|     ///
 | ||||
|     /// Note that this is UDP, which means that the open call can succeed even if the display is unreachable.
 | ||||
|     ///
 | ||||
|     /// The address of the display in CCCB is `172.23.42.29:2342`.
 | ||||
|     ///
 | ||||
|     /// # Errors
 | ||||
|     ///
 | ||||
|     /// Any errors resulting from binding the udp socket.
 | ||||
|     ///
 | ||||
|     /// # Examples
 | ||||
|     /// ```rust
 | ||||
|     ///  let connection = servicepoint::Connection::open("172.23.42.29:2342")
 | ||||
|     ///  let connection = servicepoint::Connection::open("127.0.0.1:2342")
 | ||||
|     ///     .expect("connection failed");
 | ||||
|     /// ```
 | ||||
|     pub fn open(addr: impl ToSocketAddrs + Debug) -> std::io::Result<Self> { | ||||
|         info!("connecting to {addr:?}"); | ||||
|         let socket = UdpSocket::bind("0.0.0.0:0")?; | ||||
|     #[cfg(feature = "protocol_udp")] | ||||
|     pub fn open( | ||||
|         addr: impl std::net::ToSocketAddrs + Debug, | ||||
|     ) -> std::io::Result<Self> { | ||||
|         log::info!("connecting to {addr:?}"); | ||||
|         let socket = std::net::UdpSocket::bind("0.0.0.0:0")?; | ||||
|         socket.connect(addr)?; | ||||
|         Ok(Self::Udp(socket)) | ||||
|     } | ||||
| 
 | ||||
|     /// Open a new WebSocket and connect to the provided host.
 | ||||
|     ///
 | ||||
|     /// Requires the feature "protocol_websocket" which is disabled by default.
 | ||||
|     ///
 | ||||
|     /// # Examples
 | ||||
|     ///
 | ||||
|     /// ```no_run
 | ||||
|     /// use tungstenite::http::Uri;
 | ||||
|     /// use servicepoint::{Command, Connection};
 | ||||
|     /// let uri = "ws://localhost:8080".parse().unwrap();
 | ||||
|     /// let mut connection = Connection::open_websocket(uri)
 | ||||
|     ///     .expect("could not connect");
 | ||||
|     /// connection.send_mut(Command::Clear)
 | ||||
|     ///     .expect("send failed");
 | ||||
|     /// ```
 | ||||
|     #[cfg(feature = "protocol_websocket")] | ||||
|     pub fn open_websocket( | ||||
|         uri: tungstenite::http::Uri, | ||||
|     ) -> tungstenite::Result<Self> { | ||||
|         use tungstenite::{ | ||||
|             client::IntoClientRequest, connect, ClientRequestBuilder, | ||||
|         }; | ||||
| 
 | ||||
|         log::info!("connecting to {uri:?}"); | ||||
| 
 | ||||
|         let request = ClientRequestBuilder::new(uri).into_client_request()?; | ||||
|         let (sock, _) = connect(request)?; | ||||
|         Ok(Self::WebSocket(sock)) | ||||
|     } | ||||
| 
 | ||||
|     /// Send something packet-like to the display. Usually this is in the form of a Command.
 | ||||
|     ///
 | ||||
|     /// This variant can only be used for connections that support immutable send, e.g. [Connection::Udp].
 | ||||
|     ///
 | ||||
|     /// If you want to be able to switch the protocol, you should use [Self::send_mut] instead.
 | ||||
|     ///
 | ||||
|     /// # Arguments
 | ||||
|     ///
 | ||||
|     /// - `packet`: the packet-like to send
 | ||||
|     ///
 | ||||
|     /// returns: true if packet was sent, otherwise false
 | ||||
|     ///
 | ||||
|     /// # Panics
 | ||||
|     ///
 | ||||
|     /// If the connection does not support immutable send, e.g. for [Connection::WebSocket].
 | ||||
|     ///
 | ||||
|     /// # Examples
 | ||||
|     ///
 | ||||
|     /// ```rust
 | ||||
|     ///  let connection = servicepoint::Connection::Fake;
 | ||||
|     ///  // turn off all pixels on display
 | ||||
|     ///  connection.send(servicepoint::Command::Clear)
 | ||||
|     ///      .expect("send failed");
 | ||||
|     /// ```
 | ||||
|     pub fn send(&self, packet: impl Into<Packet>) -> Result<(), SendError> { | ||||
|         let packet = packet.into(); | ||||
|         log::debug!("sending {packet:?}"); | ||||
|         let data: Vec<u8> = packet.into(); | ||||
|         match self { | ||||
|             #[cfg(feature = "protocol_udp")] | ||||
|             Connection::Udp(socket) => { | ||||
|                 socket | ||||
|                     .send(&data) | ||||
|                     .map_err(SendError::IoError) | ||||
|                     .map(move |_| ()) // ignore Ok value
 | ||||
|             } | ||||
|             Connection::Fake => { | ||||
|                 let _ = data; | ||||
|                 Ok(()) | ||||
|             } | ||||
|             #[allow(unreachable_patterns)] // depends on features
 | ||||
|             _ => { | ||||
|                 panic!("Connection {:?} does not support immutable send", self) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Send something packet-like to the display. Usually this is in the form of a Command.
 | ||||
|     ///
 | ||||
|     /// This variant has to be used for connections that do not support immutable send, e.g. [Connection::WebSocket].
 | ||||
|     ///
 | ||||
|     /// If you want to be able to switch the protocol, you should use this variant.
 | ||||
|     ///
 | ||||
|     /// # Arguments
 | ||||
|     ///
 | ||||
|     /// - `packet`: the packet-like to send
 | ||||
|  | @ -58,23 +176,70 @@ impl Connection { | |||
|     /// # Examples
 | ||||
|     ///
 | ||||
|     /// ```rust
 | ||||
|     /// # let connection = servicepoint::Connection::Fake;
 | ||||
|     ///  let mut connection = servicepoint::Connection::FakeMutableSend;
 | ||||
|     ///  // turn off all pixels on display
 | ||||
|     ///  connection.send(servicepoint::Command::Clear)
 | ||||
|     ///  connection.send_mut(servicepoint::Command::Clear)
 | ||||
|     ///      .expect("send failed");
 | ||||
|     /// ```
 | ||||
|     pub fn send(&self, packet: impl Into<Packet>) -> Result<(), SendError> { | ||||
|         let packet = packet.into(); | ||||
|         debug!("sending {packet:?}"); | ||||
|         let data: Vec<u8> = packet.into(); | ||||
|     pub fn send_mut( | ||||
|         &mut self, | ||||
|         packet: impl Into<Packet>, | ||||
|     ) -> Result<(), SendError> { | ||||
|         match self { | ||||
|             Connection::Udp(socket) => { | ||||
|             #[cfg(feature = "protocol_websocket")] | ||||
|             Connection::WebSocket(socket) => { | ||||
|                 let packet = packet.into(); | ||||
|                 log::debug!("sending {packet:?}"); | ||||
|                 let data: Vec<u8> = packet.into(); | ||||
|                 socket | ||||
|                     .send(&data) | ||||
|                     .map_err(SendError::IoError) | ||||
|                     .map(move |_| ()) // ignore Ok value
 | ||||
|                     .send(tungstenite::Message::Binary(data)) | ||||
|                     .map_err(SendError::WebsocketError) | ||||
|             } | ||||
|             Connection::Fake => Ok(()), | ||||
|             Connection::FakeMutableSend => { | ||||
|                 let packet = packet.into(); | ||||
|                 log::debug!("sending {packet:?}"); | ||||
|                 let data: Vec<u8> = packet.into(); | ||||
|                 let _ = data; | ||||
|                 Ok(()) | ||||
|             } | ||||
|             _ => self.send(packet), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Drop for Connection { | ||||
|     fn drop(&mut self) { | ||||
|         #[cfg(feature = "protocol_websocket")] | ||||
|         if let Connection::WebSocket(sock) = self { | ||||
|             _ = sock.close(None); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
|     use crate::packet::*; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn send_fake() { | ||||
|         let data: &[u8] = &[0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; | ||||
|         let packet = Packet::try_from(data).unwrap(); | ||||
|         Connection::Fake.send(packet).unwrap() | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn send_fake_mutable() { | ||||
|         let data: &[u8] = &[0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; | ||||
|         let packet = Packet::try_from(data).unwrap(); | ||||
|         Connection::FakeMutableSend.send_mut(packet).unwrap() | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     #[should_panic] | ||||
|     fn send_fake_mutable_panic() { | ||||
|         let data: &[u8] = &[0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; | ||||
|         let packet = Packet::try_from(data).unwrap(); | ||||
|         Connection::FakeMutableSend.send(packet).unwrap() | ||||
|     } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										221
									
								
								crates/servicepoint/src/cp437.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								crates/servicepoint/src/cp437.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,221 @@ | |||
| use crate::cp437::Cp437LoadError::InvalidChar; | ||||
| use crate::{Grid, PrimitiveGrid}; | ||||
| use std::collections::HashMap; | ||||
| 
 | ||||
| /// A grid containing codepage 437 characters.
 | ||||
| ///
 | ||||
| /// The encoding is currently not enforced.
 | ||||
| pub type Cp437Grid = PrimitiveGrid<u8>; | ||||
| 
 | ||||
| /// A grid containing UTF-8 characters.
 | ||||
| pub type CharGrid = PrimitiveGrid<char>; | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub enum Cp437LoadError { | ||||
|     InvalidChar { index: usize, char: char }, | ||||
| } | ||||
| 
 | ||||
| impl Cp437Grid { | ||||
|     /// Load an ASCII-only [&str] into a [Cp437Grid] of specified width.
 | ||||
|     ///
 | ||||
|     /// # Panics
 | ||||
|     ///
 | ||||
|     /// - for width == 0
 | ||||
|     /// - on empty strings
 | ||||
|     pub fn load_ascii( | ||||
|         value: &str, | ||||
|         width: usize, | ||||
|         wrap: bool, | ||||
|     ) -> Result<Self, Cp437LoadError> { | ||||
|         assert!(width > 0); | ||||
|         assert!(!value.is_empty()); | ||||
| 
 | ||||
|         let mut chars = { | ||||
|             let mut x = 0; | ||||
|             let mut y = 0; | ||||
| 
 | ||||
|             for (index, char) in value.chars().enumerate() { | ||||
|                 if !char.is_ascii() { | ||||
|                     return Err(InvalidChar { index, char }); | ||||
|                 } | ||||
| 
 | ||||
|                 let is_lf = char == '\n'; | ||||
|                 if is_lf || (wrap && x == width) { | ||||
|                     y += 1; | ||||
|                     x = 0; | ||||
|                     if is_lf { | ||||
|                         continue; | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 x += 1; | ||||
|             } | ||||
| 
 | ||||
|             Cp437Grid::new(width, y + 1) | ||||
|         }; | ||||
| 
 | ||||
|         let mut x = 0; | ||||
|         let mut y = 0; | ||||
|         for char in value.chars().map(move |c| c as u8) { | ||||
|             let is_lf = char == b'\n'; | ||||
|             if is_lf || (wrap && x == width) { | ||||
|                 y += 1; | ||||
|                 x = 0; | ||||
|                 if is_lf { | ||||
|                     continue; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if wrap || x < width { | ||||
|                 chars.set(x, y, char); | ||||
|             } | ||||
|             x += 1; | ||||
|         } | ||||
| 
 | ||||
|         Ok(chars) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[allow(unused)] // depends on features
 | ||||
| pub use feature_cp437::*; | ||||
| 
 | ||||
| #[cfg(feature = "cp437")] | ||||
| mod feature_cp437 { | ||||
|     use super::*; | ||||
| 
 | ||||
|     /// An array of 256 elements, mapping most of the CP437 values to UTF-8 characters
 | ||||
|     ///
 | ||||
|     /// Mostly follows CP437, except for:
 | ||||
|     ///  * 0x0A & 0x0D are kept for use as line endings.
 | ||||
|     ///  * 0x1A is used for SAUCE.
 | ||||
|     ///  * 0x1B is used for ANSI escape sequences.
 | ||||
|     ///
 | ||||
|     /// These exclusions should be fine since most programs can't even use them
 | ||||
|     /// without issues. And this makes rendering simpler too.
 | ||||
|     ///
 | ||||
|     /// See <https://en.wikipedia.org/wiki/Code_page_437#Character_set>
 | ||||
|     ///
 | ||||
|     /// Copied from https://github.com/kip93/cp437-tools. License: GPL-3.0
 | ||||
|     #[rustfmt::skip] | ||||
|     const CP437_TO_UTF8: [char; 256] = [ | ||||
|         /* 0X */ '\0', '☺', '☻', '♥', '♦', '♣', '♠', '•', '◘', '○', '\n', '♂', '♀', '\r', '♫', '☼', | ||||
|         /* 1X */ '►', '◄', '↕', '‼', '¶', '§', '▬', '↨', '↑', '↓', '', '', '∟', '↔',  '▲', '▼', | ||||
|         /* 2X */ ' ', '!', '"', '#', '$', '%', '&', '\'','(', ')', '*', '+', ',', '-', '.', '/', | ||||
|         /* 3X */ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', | ||||
|         /* 4X */ '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', | ||||
|         /* 5X */ 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\',']', '^', '_', | ||||
|         /* 6X */ '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', | ||||
|         /* 7X */ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '⌂', | ||||
|         /* 8X */ 'Ç', 'ü', 'é', 'â', 'ä', 'à', 'å', 'ç', 'ê', 'ë', 'è', 'ï', 'î', 'ì', 'Ä', 'Å', | ||||
|         /* 9X */ 'É', 'æ', 'Æ', 'ô', 'ö', 'ò', 'û', 'ù', 'ÿ', 'Ö', 'Ü', '¢', '£', '¥', '₧', 'ƒ', | ||||
|         /* AX */ 'á', 'í', 'ó', 'ú', 'ñ', 'Ñ', 'ª', 'º', '¿', '⌐', '¬', '½', '¼', '¡', '«', '»', | ||||
|         /* BX */ '░', '▒', '▓', '│', '┤', '╡', '╢', '╖', '╕', '╣', '║', '╗', '╝', '╜', '╛', '┐', | ||||
|         /* CX */ '└', '┴', '┬', '├', '─', '┼', '╞', '╟', '╚', '╔', '╩', '╦', '╠', '═', '╬', '╧', | ||||
|         /* DX */ '╨', '╤', '╥', '╙', '╘', '╒', '╓', '╫', '╪', '┘', '┌', '█', '▄', '▌', '▐', '▀', | ||||
|         /* EX */ 'α', 'ß', 'Γ', 'π', 'Σ', 'σ', 'µ', 'τ', 'Φ', 'Θ', 'Ω', 'δ', '∞', 'φ', 'ε', '∩', | ||||
|         /* FX */ '≡', '±', '≥', '≤', '⌠', '⌡', '÷', '≈', '°', '∙', '·', '√', 'ⁿ', '²', '■', ' ', | ||||
|     ]; | ||||
| 
 | ||||
|     const UTF8_TO_CP437: once_cell::sync::Lazy<HashMap<char, u8>> = | ||||
|         once_cell::sync::Lazy::new(|| { | ||||
|             let pairs = CP437_TO_UTF8 | ||||
|                 .iter() | ||||
|                 .enumerate() | ||||
|                 .map(move |(index, char)| (*char, index as u8)); | ||||
|             HashMap::from_iter(pairs) | ||||
|         }); | ||||
| 
 | ||||
|     const MISSING_CHAR_CP437: u8 = 0x3F; | ||||
| 
 | ||||
|     impl From<&Cp437Grid> for CharGrid { | ||||
|         fn from(value: &Cp437Grid) -> Self { | ||||
|             let mut grid = Self::new(value.width(), value.height()); | ||||
| 
 | ||||
|             for y in 0..grid.height() { | ||||
|                 for x in 0..grid.width() { | ||||
|                     let converted = CP437_TO_UTF8[value.get(x, y) as usize]; | ||||
|                     grid.set(x, y, converted); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             grid | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     impl From<&CharGrid> for Cp437Grid { | ||||
|         fn from(value: &CharGrid) -> Self { | ||||
|             let mut grid = Self::new(value.width(), value.height()); | ||||
| 
 | ||||
|             for y in 0..grid.height() { | ||||
|                 for x in 0..grid.width() { | ||||
|                     let char = value.get(x, y); | ||||
|                     let converted = *UTF8_TO_CP437 | ||||
|                         .get(&char) | ||||
|                         .unwrap_or(&MISSING_CHAR_CP437); | ||||
|                     grid.set(x, y, converted); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             grid | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     impl From<&str> for CharGrid { | ||||
|         fn from(value: &str) -> Self { | ||||
|             let value = value.replace("\r\n", "\n"); | ||||
|             let lines = value.split('\n').collect::<Vec<_>>(); | ||||
|             let width = | ||||
|                 lines.iter().fold(0, move |a, x| std::cmp::max(a, x.len())); | ||||
| 
 | ||||
|             let mut grid = Self::new(width, lines.len()); | ||||
|             for (y, line) in lines.iter().enumerate() { | ||||
|                 for (x, char) in line.chars().enumerate() { | ||||
|                     grid.set(x, y, char); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             grid | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn load_ascii_nowrap() { | ||||
|         let chars = ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd'] | ||||
|             .map(move |c| c as u8); | ||||
|         let expected = Cp437Grid::load(5, 2, &chars); | ||||
| 
 | ||||
|         let actual = Cp437Grid::load_ascii("Hello,\nWorld!", 5, false).unwrap(); | ||||
|         // comma will be removed because line is too long and wrap is off
 | ||||
|         assert_eq!(actual, expected); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn load_ascii_wrap() { | ||||
|         let chars = ['H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd'] | ||||
|             .map(move |c| c as u8); | ||||
|         let expected = Cp437Grid::load(5, 2, &chars); | ||||
| 
 | ||||
|         let actual = Cp437Grid::load_ascii("HelloWorld", 5, true).unwrap(); | ||||
|         // line break will be added
 | ||||
|         assert_eq!(actual, expected); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| #[cfg(feature = "cp437")] | ||||
| mod tests_feature_cp437 { | ||||
|     use crate::{CharGrid, Cp437Grid}; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn round_trip_cp437() { | ||||
|         let utf8 = CharGrid::load(2, 2, &['Ä', 'x', '\n', '$']); | ||||
|         let cp437 = Cp437Grid::from(&utf8); | ||||
|         let actual = CharGrid::from(&cp437); | ||||
|         assert_eq!(actual, utf8); | ||||
|     } | ||||
| } | ||||
|  | @ -79,12 +79,12 @@ pub trait Grid<T> { | |||
|         assert!( | ||||
|             x < self.width(), | ||||
|             "cannot access index [{x}, {y}] because x is outside of bounds 0..{}", | ||||
|             self.width() | ||||
|             self.width() - 1 | ||||
|         ); | ||||
|         assert!( | ||||
|             y < self.height(), | ||||
|             "cannot access byte [{x}, {y}] because y is outside of bounds 0..{}", | ||||
|             self.height() | ||||
|             "cannot access index [{x}, {y}] because y is outside of bounds 0..{}", | ||||
|             self.height() - 1 | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,9 @@ | |||
| //! Abstractions for the UDP protocol of the CCCB servicepoint display.
 | ||||
| //!
 | ||||
| //! Your starting point is a [Connection] to the display.
 | ||||
| //! With a connection, you can send [Command]s.
 | ||||
| //! When received, the display will update the state of the pixels.
 | ||||
| //!
 | ||||
| //! # Examples
 | ||||
| //!
 | ||||
| //! ```rust
 | ||||
|  | @ -37,13 +41,13 @@ pub use bitvec; | |||
| use bitvec::prelude::{BitVec, Msb0}; | ||||
| 
 | ||||
| pub use crate::brightness::{Brightness, BrightnessGrid}; | ||||
| pub use crate::command::{Command, Cp437Grid, Offset}; | ||||
| pub use crate::command::{Command, Offset}; | ||||
| pub use crate::compression_code::CompressionCode; | ||||
| pub use crate::connection::Connection; | ||||
| pub use crate::cp437::{CharGrid, Cp437Grid}; | ||||
| pub use crate::data_ref::DataRef; | ||||
| pub use crate::grid::Grid; | ||||
| pub use crate::origin::{Origin, Pixels, Tiles}; | ||||
| pub use crate::packet::{Header, Packet, Payload}; | ||||
| pub use crate::pixel_grid::PixelGrid; | ||||
| pub use crate::primitive_grid::PrimitiveGrid; | ||||
| 
 | ||||
|  | @ -55,10 +59,11 @@ mod command_code; | |||
| mod compression; | ||||
| mod compression_code; | ||||
| mod connection; | ||||
| mod cp437; | ||||
| mod data_ref; | ||||
| mod grid; | ||||
| mod origin; | ||||
| mod packet; | ||||
| pub mod packet; | ||||
| mod pixel_grid; | ||||
| mod primitive_grid; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| use crate::TILE_SIZE; | ||||
| use std::marker::PhantomData; | ||||
| 
 | ||||
| /// An origin marks the top left position of a window sent to the display.
 | ||||
|  | @ -11,7 +12,14 @@ pub struct Origin<Unit: DisplayUnit> { | |||
| } | ||||
| 
 | ||||
| impl<Unit: DisplayUnit> Origin<Unit> { | ||||
|     /// Create a new `Origin` instance for the provided position.
 | ||||
|     /// Top-left. Equivalent to `Origin::new(0, 0)`.
 | ||||
|     pub const ZERO: Self = Self { | ||||
|         x: 0, | ||||
|         y: 0, | ||||
|         phantom_data: PhantomData, | ||||
|     }; | ||||
| 
 | ||||
|     /// Create a new [Origin] instance for the provided position.
 | ||||
|     pub fn new(x: usize, y: usize) -> Self { | ||||
|         Self { | ||||
|             x, | ||||
|  | @ -46,3 +54,69 @@ pub struct Tiles(); | |||
| impl DisplayUnit for Pixels {} | ||||
| 
 | ||||
| impl DisplayUnit for Tiles {} | ||||
| 
 | ||||
| impl From<&Origin<Tiles>> for Origin<Pixels> { | ||||
|     fn from(value: &Origin<Tiles>) -> Self { | ||||
|         Self { | ||||
|             x: value.x * TILE_SIZE, | ||||
|             y: value.y * TILE_SIZE, | ||||
|             phantom_data: PhantomData, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl TryFrom<&Origin<Pixels>> for Origin<Tiles> { | ||||
|     type Error = (); | ||||
| 
 | ||||
|     fn try_from(value: &Origin<Pixels>) -> Result<Self, Self::Error> { | ||||
|         let (x, x_rem) = (value.x / TILE_SIZE, value.x % TILE_SIZE); | ||||
|         if x_rem != 0 { | ||||
|             return Err(()); | ||||
|         } | ||||
|         let (y, y_rem) = (value.y / TILE_SIZE, value.y % TILE_SIZE); | ||||
|         if y_rem != 0 { | ||||
|             return Err(()); | ||||
|         } | ||||
| 
 | ||||
|         Ok(Self { | ||||
|             x, | ||||
|             y, | ||||
|             phantom_data: PhantomData, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn origin_tile_to_pixel() { | ||||
|         let tile: Origin<Tiles> = Origin::new(1, 2); | ||||
|         let actual: Origin<Pixels> = Origin::from(&tile); | ||||
|         let expected: Origin<Pixels> = Origin::new(8, 16); | ||||
|         assert_eq!(actual, expected); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn origin_pixel_to_tile() { | ||||
|         let pixel: Origin<Pixels> = Origin::new(8, 16); | ||||
|         let actual: Origin<Tiles> = Origin::try_from(&pixel).unwrap(); | ||||
|         let expected: Origin<Tiles> = Origin::new(1, 2); | ||||
|         assert_eq!(actual, expected); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     #[should_panic] | ||||
|     fn origin_pixel_to_tile_fail_y() { | ||||
|         let pixel: Origin<Pixels> = Origin::new(8, 15); | ||||
|         let _: Origin<Tiles> = Origin::try_from(&pixel).unwrap(); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     #[should_panic] | ||||
|     fn origin_pixel_to_tile_fail_x() { | ||||
|         let pixel: Origin<Pixels> = Origin::new(7, 16); | ||||
|         let _: Origin<Tiles> = Origin::try_from(&pixel).unwrap(); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,10 +1,34 @@ | |||
| //! Raw packet manipulation.
 | ||||
| //!
 | ||||
| //! Should probably only be used directly to use features not exposed by the library.
 | ||||
| //!
 | ||||
| //! # Examples
 | ||||
| //!
 | ||||
| //! Converting a packet to a command and back:
 | ||||
| //!
 | ||||
| //! ```rust
 | ||||
| //! use servicepoint::{Command, packet::Packet};
 | ||||
| //! # let command = Command::Clear;
 | ||||
| //! let packet: Packet = command.into();
 | ||||
| //! let command: Command = Command::try_from(packet).expect("could not read command from packet");
 | ||||
| //! ```
 | ||||
| //!
 | ||||
| //! Converting a packet to bytes and back:
 | ||||
| //!
 | ||||
| //! ```rust
 | ||||
| //! use servicepoint::{Command, packet::Packet};
 | ||||
| //! # let command = Command::Clear;
 | ||||
| //! # let packet: Packet = command.into();
 | ||||
| //! let bytes: Vec<u8> = packet.into();
 | ||||
| //! let packet = Packet::try_from(bytes).expect("could not read packet from bytes");
 | ||||
| //! ```
 | ||||
| 
 | ||||
| use std::mem::size_of; | ||||
| 
 | ||||
| use crate::command_code::CommandCode; | ||||
| use crate::compression::into_compressed; | ||||
| use crate::{ | ||||
|     Command, CompressionCode, Grid, Offset, Origin, PixelGrid, Pixels, Tiles, | ||||
|     TILE_SIZE, | ||||
|     command_code::CommandCode, Command, CompressionCode, Grid, Offset, Origin, | ||||
|     PixelGrid, Pixels, Tiles, TILE_SIZE, | ||||
| }; | ||||
| 
 | ||||
| /// A raw header.
 | ||||
|  | @ -13,8 +37,6 @@ use crate::{ | |||
| /// payload, where applicable.
 | ||||
| ///
 | ||||
| /// Because the meaning of most fields depend on the command, there are no speaking names for them.
 | ||||
| ///
 | ||||
| /// Should probably only be used directly to use features not exposed by the library.
 | ||||
| #[derive(Copy, Clone, Debug, PartialEq)] | ||||
| pub struct Header { | ||||
|     /// The first two bytes specify which command this packet represents.
 | ||||
|  | @ -38,26 +60,8 @@ pub type Payload = Vec<u8>; | |||
| ///
 | ||||
| /// Contents should probably only be used directly to use features not exposed by the library.
 | ||||
| ///
 | ||||
| /// # Examples
 | ||||
| /// You may want to use [Command] instead.
 | ||||
| ///
 | ||||
| /// Converting a packet to a command and back:
 | ||||
| ///
 | ||||
| /// ```rust
 | ||||
| /// # use servicepoint::{Command, Packet};
 | ||||
| /// # let command = Command::Clear;
 | ||||
| /// let packet: Packet = command.into();
 | ||||
| /// let command: Command = Command::try_from(packet).expect("could not read packet");
 | ||||
| /// ```
 | ||||
| ///
 | ||||
| /// Converting a packet to bytes and back:
 | ||||
| ///
 | ||||
| /// ```rust
 | ||||
| /// # use servicepoint::{Command, Packet};
 | ||||
| /// # let command = Command::Clear;
 | ||||
| /// # let packet: Packet = command.into();
 | ||||
| /// let bytes: Vec<u8> = packet.into();
 | ||||
| /// let packet = Packet::try_from(bytes).expect("could not read packet from bytes");
 | ||||
| /// ```
 | ||||
| ///
 | ||||
| #[derive(Clone, Debug, PartialEq)] | ||||
| pub struct Packet { | ||||
|  | @ -98,9 +102,9 @@ impl From<Packet> for Vec<u8> { | |||
| impl TryFrom<&[u8]> for Packet { | ||||
|     type Error = (); | ||||
| 
 | ||||
|     /// Tries to interpret the bytes as a `Packet`.
 | ||||
|     /// Tries to interpret the bytes as a [Packet].
 | ||||
|     ///
 | ||||
|     /// returns: `Error` if slice is not long enough to be a `Packet`
 | ||||
|     /// returns: `Error` if slice is not long enough to be a [Packet]
 | ||||
|     fn try_from(value: &[u8]) -> Result<Self, Self::Error> { | ||||
|         if value.len() < size_of::<Header>() { | ||||
|             return Err(()); | ||||
|  | @ -135,7 +139,7 @@ impl TryFrom<Vec<u8>> for Packet { | |||
| } | ||||
| 
 | ||||
| impl From<Command> for Packet { | ||||
|     /// Move the `Command` into a `Packet` instance for sending.
 | ||||
|     /// Move the [Command] into a [Packet] instance for sending.
 | ||||
|     #[allow(clippy::cast_possible_truncation)] | ||||
|     fn from(value: Command) -> Self { | ||||
|         match value { | ||||
|  | @ -210,7 +214,7 @@ impl From<Command> for Packet { | |||
| } | ||||
| 
 | ||||
| impl Packet { | ||||
|     /// Helper method for `BitMapLinear*`-Commands into `Packet`
 | ||||
|     /// Helper method for `BitMapLinear*`-Commands into [Packet]
 | ||||
|     #[allow(clippy::cast_possible_truncation)] | ||||
|     fn bitmap_linear_into_packet( | ||||
|         command: CommandCode, | ||||
|  | @ -312,7 +316,7 @@ impl Packet { | |||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use crate::{Header, Packet}; | ||||
|     use super::*; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn round_trip() { | ||||
|  | @ -327,7 +331,7 @@ mod tests { | |||
|             payload: vec![42u8; 23], | ||||
|         }; | ||||
|         let data: Vec<u8> = p.into(); | ||||
|         let p = Packet::try_from(&*data).unwrap(); | ||||
|         let p = Packet::try_from(data).unwrap(); | ||||
|         assert_eq!( | ||||
|             p, | ||||
|             Packet { | ||||
|  |  | |||
|  | @ -13,14 +13,14 @@ pub struct PixelGrid { | |||
| } | ||||
| 
 | ||||
| impl PixelGrid { | ||||
|     /// Creates a new `PixelGrid` with the specified dimensions.
 | ||||
|     /// Creates a new [PixelGrid] with the specified dimensions.
 | ||||
|     ///
 | ||||
|     /// # Arguments
 | ||||
|     ///
 | ||||
|     /// - `width`: size in pixels in x-direction
 | ||||
|     /// - `height`: size in pixels in y-direction
 | ||||
|     ///
 | ||||
|     /// returns: `PixelGrid` initialized to all pixels off
 | ||||
|     /// returns: [PixelGrid] initialized to all pixels off
 | ||||
|     ///
 | ||||
|     /// # Panics
 | ||||
|     ///
 | ||||
|  | @ -40,14 +40,14 @@ impl PixelGrid { | |||
|         Self::new(PIXEL_WIDTH, PIXEL_HEIGHT) | ||||
|     } | ||||
| 
 | ||||
|     /// Loads a `PixelGrid` with the specified dimensions from the provided data.
 | ||||
|     /// Loads a [PixelGrid] with the specified dimensions from the provided data.
 | ||||
|     ///
 | ||||
|     /// # Arguments
 | ||||
|     ///
 | ||||
|     /// - `width`: size in pixels in x-direction
 | ||||
|     /// - `height`: size in pixels in y-direction
 | ||||
|     ///
 | ||||
|     /// returns: `PixelGrid` that contains a copy of the provided data
 | ||||
|     /// returns: [PixelGrid] that contains a copy of the provided data
 | ||||
|     ///
 | ||||
|     /// # Panics
 | ||||
|     ///
 | ||||
|  | @ -64,7 +64,7 @@ impl PixelGrid { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Iterate over all cells in `PixelGrid`.
 | ||||
|     /// Iterate over all cells in [PixelGrid].
 | ||||
|     ///
 | ||||
|     /// Order is equivalent to the following loop:
 | ||||
|     /// ```
 | ||||
|  | @ -80,7 +80,7 @@ impl PixelGrid { | |||
|         self.bit_vec.iter().by_refs() | ||||
|     } | ||||
| 
 | ||||
|     /// Iterate over all cells in `PixelGrid` mutably.
 | ||||
|     /// Iterate over all cells in [PixelGrid] mutably.
 | ||||
|     ///
 | ||||
|     /// Order is equivalent to the following loop:
 | ||||
|     /// ```
 | ||||
|  | @ -107,7 +107,7 @@ impl PixelGrid { | |||
|         self.bit_vec.iter_mut() | ||||
|     } | ||||
| 
 | ||||
|     /// Iterate over all rows in `PixelGrid` top to bottom.
 | ||||
|     /// Iterate over all rows in [PixelGrid] top to bottom.
 | ||||
|     pub fn iter_rows(&self) -> IterRows { | ||||
|         IterRows { | ||||
|             pixel_grid: self, | ||||
|  | @ -117,7 +117,7 @@ impl PixelGrid { | |||
| } | ||||
| 
 | ||||
| impl Grid<bool> for PixelGrid { | ||||
|     /// Sets the value of the specified position in the `PixelGrid`.
 | ||||
|     /// Sets the value of the specified position in the [PixelGrid].
 | ||||
|     ///
 | ||||
|     /// # Arguments
 | ||||
|     ///
 | ||||
|  | @ -139,7 +139,7 @@ impl Grid<bool> for PixelGrid { | |||
|         self.bit_vec[x + y * self.width] | ||||
|     } | ||||
| 
 | ||||
|     /// Sets the state of all pixels in the `PixelGrid`.
 | ||||
|     /// Sets the state of all pixels in the [PixelGrid].
 | ||||
|     ///
 | ||||
|     /// # Arguments
 | ||||
|     ///
 | ||||
|  | @ -169,7 +169,7 @@ impl DataRef<u8> for PixelGrid { | |||
| } | ||||
| 
 | ||||
| impl From<PixelGrid> for Vec<u8> { | ||||
|     /// Turns a `PixelGrid` into the underlying `Vec<u8>`.
 | ||||
|     /// Turns a [PixelGrid] into the underlying [`Vec<u8>`].
 | ||||
|     fn from(value: PixelGrid) -> Self { | ||||
|         value.bit_vec.into() | ||||
|     } | ||||
|  |  | |||
|  | @ -14,14 +14,14 @@ pub struct PrimitiveGrid<T: PrimitiveGridType> { | |||
| } | ||||
| 
 | ||||
| impl<T: PrimitiveGridType> PrimitiveGrid<T> { | ||||
|     /// Creates a new `PrimitiveGrid` with the specified dimensions.
 | ||||
|     /// Creates a new [PrimitiveGrid] with the specified dimensions.
 | ||||
|     ///
 | ||||
|     /// # Arguments
 | ||||
|     ///
 | ||||
|     /// - width: size in x-direction
 | ||||
|     /// - height: size in y-direction
 | ||||
|     ///
 | ||||
|     /// returns: `PrimitiveGrid` initialized to default value.
 | ||||
|     /// returns: [PrimitiveGrid] initialized to default value.
 | ||||
|     pub fn new(width: usize, height: usize) -> Self { | ||||
|         Self { | ||||
|             data: vec![Default::default(); width * height], | ||||
|  | @ -30,9 +30,9 @@ impl<T: PrimitiveGridType> PrimitiveGrid<T> { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Loads a `PrimitiveGrid` with the specified dimensions from the provided data.
 | ||||
|     /// Loads a [PrimitiveGrid] with the specified dimensions from the provided data.
 | ||||
|     ///
 | ||||
|     /// returns: `PrimitiveGrid` that contains a copy of the provided data
 | ||||
|     /// returns: [PrimitiveGrid] that contains a copy of the provided data
 | ||||
|     ///
 | ||||
|     /// # Panics
 | ||||
|     ///
 | ||||
|  | @ -47,7 +47,7 @@ impl<T: PrimitiveGridType> PrimitiveGrid<T> { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Iterate over all cells in `PrimitiveGrid`.
 | ||||
|     /// Iterate over all cells in [PrimitiveGrid].
 | ||||
|     ///
 | ||||
|     /// Order is equivalent to the following loop:
 | ||||
|     /// ```
 | ||||
|  | @ -63,7 +63,7 @@ impl<T: PrimitiveGridType> PrimitiveGrid<T> { | |||
|         self.data.iter() | ||||
|     } | ||||
| 
 | ||||
|     /// Iterate over all rows in `PrimitiveGrid` top to bottom.
 | ||||
|     /// Iterate over all rows in [PrimitiveGrid] top to bottom.
 | ||||
|     pub fn iter_rows(&self) -> IterRows<T> { | ||||
|         IterRows { | ||||
|             byte_grid: self, | ||||
|  | @ -168,7 +168,7 @@ impl<T: PrimitiveGridType> DataRef<T> for PrimitiveGrid<T> { | |||
| } | ||||
| 
 | ||||
| impl<T: PrimitiveGridType> From<PrimitiveGrid<T>> for Vec<T> { | ||||
|     /// Turn into the underlying `Vec<u8>` containing the rows of bytes.
 | ||||
|     /// Turn into the underlying [`Vec<u8>`] containing the rows of bytes.
 | ||||
|     fn from(value: PrimitiveGrid<T>) -> Self { | ||||
|         value.data | ||||
|     } | ||||
|  |  | |||
|  | @ -17,9 +17,12 @@ crate-type = ["staticlib", "cdylib", "rlib"] | |||
| cbindgen = "0.27.0" | ||||
| 
 | ||||
| [dependencies.servicepoint] | ||||
| version = "0.8.0" | ||||
| version = "0.9.0" | ||||
| path = "../servicepoint" | ||||
| features = ["all_compressions"] | ||||
| 
 | ||||
| [lints] | ||||
| workspace = true | ||||
| 
 | ||||
| [package.metadata.docs.rs] | ||||
| all-features = true | ||||
|  |  | |||
|  | @ -12,5 +12,6 @@ fn main() { | |||
|     let mut cc = cc::Build::new(); | ||||
|     cc.file("src/main.c"); | ||||
|     cc.include(&sp_include); | ||||
|     cc.opt_level(2); | ||||
|     cc.compile("lang_c"); | ||||
| } | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| /* Generated with cbindgen:0.26.0 */ | ||||
| /* Generated with cbindgen:0.27.0 */ | ||||
| 
 | ||||
| /* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */ | ||||
| 
 | ||||
|  | @ -1355,5 +1355,5 @@ struct SPByteSlice sp_pixel_grid_unsafe_data_ref(struct SPPixelGrid *pixel_grid) | |||
| size_t sp_pixel_grid_width(const struct SPPixelGrid *pixel_grid); | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| } // extern "C"
 | ||||
| #endif // __cplusplus
 | ||||
| }  // extern "C"
 | ||||
| #endif  // __cplusplus
 | ||||
|  |  | |||
|  | @ -40,7 +40,9 @@ pub unsafe extern "C" fn sp_cp437_grid_new( | |||
|     width: usize, | ||||
|     height: usize, | ||||
| ) -> *mut SPCp437Grid { | ||||
|     Box::into_raw(Box::new(SPCp437Grid(servicepoint::Cp437Grid::new(width, height)))) | ||||
|     Box::into_raw(Box::new(SPCp437Grid(servicepoint::Cp437Grid::new( | ||||
|         width, height, | ||||
|     )))) | ||||
| } | ||||
| 
 | ||||
| /// Loads a `SPCp437Grid` with the specified dimensions from the provided data.
 | ||||
|  | @ -67,7 +69,9 @@ pub unsafe extern "C" fn sp_cp437_grid_load( | |||
|     data_length: usize, | ||||
| ) -> *mut SPCp437Grid { | ||||
|     let data = std::slice::from_raw_parts(data, data_length); | ||||
|     Box::into_raw(Box::new(SPCp437Grid(servicepoint::Cp437Grid::load(width, height, data)))) | ||||
|     Box::into_raw(Box::new(SPCp437Grid(servicepoint::Cp437Grid::load( | ||||
|         width, height, data, | ||||
|     )))) | ||||
| } | ||||
| 
 | ||||
| /// Clones a `SPCp437Grid`.
 | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ use std::ptr::null_mut; | |||
| use crate::SPCommand; | ||||
| 
 | ||||
| /// The raw packet
 | ||||
| pub struct SPPacket(pub(crate) servicepoint::Packet); | ||||
| pub struct SPPacket(pub(crate) servicepoint::packet::Packet); | ||||
| 
 | ||||
| /// Turns a `SPCommand` into a `SPPacket`.
 | ||||
| /// The `SPCommand` gets consumed.
 | ||||
|  | @ -49,7 +49,7 @@ pub unsafe extern "C" fn sp_packet_try_load( | |||
|     length: usize, | ||||
| ) -> *mut SPPacket { | ||||
|     let data = std::slice::from_raw_parts(data, length); | ||||
|     match servicepoint::Packet::try_from(data) { | ||||
|     match servicepoint::packet::Packet::try_from(data) { | ||||
|         Err(_) => null_mut(), | ||||
|         Ok(packet) => Box::into_raw(Box::new(SPPacket(packet))), | ||||
|     } | ||||
|  |  | |||
|  | @ -13,8 +13,8 @@ test = false | |||
| csbindgen = "1.9.3" | ||||
| 
 | ||||
| [dependencies] | ||||
| servicepoint_binding_c = { version = "0.8.0", path = "../servicepoint_binding_c" } | ||||
| servicepoint = { version = "0.8.0", path = "../servicepoint" } | ||||
| servicepoint_binding_c = { version = "0.9.0", path = "../servicepoint_binding_c" } | ||||
| servicepoint = { version = "0.9.0", path = "../servicepoint" } | ||||
| 
 | ||||
| [lints] | ||||
| workspace = true | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ | |||
| 
 | ||||
|     <PropertyGroup> | ||||
|         <PackageId>ServicePoint</PackageId> | ||||
|         <Version>0.8.0</Version> | ||||
|         <Version>0.9.0</Version> | ||||
|         <Authors>Repository Authors</Authors> | ||||
|         <Company>None</Company> | ||||
|         <Product>ServicePoint</Product> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Vinzenz Schroeter
						Vinzenz Schroeter