raintron 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/bump +30 -0
- data/bin/raintron +6 -0
- data/lib/app_config/app_config.rb +33 -0
- data/lib/cli/cli.rb +69 -0
- data/lib/commands/doctor.rb +39 -0
- data/lib/commands/icons.rb +34 -0
- data/lib/commands/install.rb +153 -0
- data/lib/commands/press.rb +105 -0
- data/lib/generator/generator.rb +39 -0
- data/lib/icons/icns_writer.rb +42 -0
- data/lib/icons/ico_writer.rb +29 -0
- data/lib/icons/icon_generator.rb +66 -0
- data/lib/icons/png_decoder.rb +138 -0
- data/lib/icons/png_writer.rb +37 -0
- data/lib/icons/renderer.rb +39 -0
- data/lib/icons/resampler.rb +39 -0
- data/lib/launcher/launcher.rb +98 -0
- data/lib/launcher/loading.html +323 -0
- data/lib/preflight/preflight.rb +43 -0
- data/lib/raintron.rb +24 -0
- data/lib/shims/console_shim.rb +19 -0
- data/lib/shims/packed_fs_shim.rb +65 -0
- data/lib/shims/shims.rb +34 -0
- data/lib/version.rb +5 -0
- data/lib/window_chrome/window_chrome.rb +60 -0
- data/templates/app/Contents/Info.plist.erb +24 -0
- data/templates/app/Contents/MacOS/launch.erb +6 -0
- data/templates/bin/desktop.erb +22 -0
- data/templates/github/workflows/build.yml.erb +100 -0
- data/templates/icons/app.desktop.erb +10 -0
- metadata +118 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>raindown</title>
|
|
7
|
+
<style>
|
|
8
|
+
body { margin: 0; height: 100vh; overflow: hidden; background: #0b0e14; }
|
|
9
|
+
canvas { display: block; }
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<canvas></canvas>
|
|
14
|
+
<script>
|
|
15
|
+
{
|
|
16
|
+
// A cell is the unit of the interface: one monospace character drawn over
|
|
17
|
+
// its own small bitmap. Transparent pixels (0) let the background show through.
|
|
18
|
+
const CELL_W = 8
|
|
19
|
+
const CELL_H = 16
|
|
20
|
+
|
|
21
|
+
const packedColors = new Map()
|
|
22
|
+
|
|
23
|
+
// Packs '#rrggbb' into the 32-bit ABGR layout used by ImageData on little-endian machines.
|
|
24
|
+
function packColor(hex) {
|
|
25
|
+
if (!packedColors.has(hex)) {
|
|
26
|
+
const r = parseInt(hex.slice(1, 3), 16)
|
|
27
|
+
const g = parseInt(hex.slice(3, 5), 16)
|
|
28
|
+
const b = parseInt(hex.slice(5, 7), 16)
|
|
29
|
+
packedColors.set(hex, ((255 << 24) | (b << 16) | (g << 8) | r) >>> 0)
|
|
30
|
+
}
|
|
31
|
+
return packedColors.get(hex)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DEFAULT_FG = '#d8dee9'
|
|
35
|
+
const DEFAULT_BG = packColor('#0b0e14')
|
|
36
|
+
|
|
37
|
+
class Cell {
|
|
38
|
+
constructor(col, row) {
|
|
39
|
+
this.col = col
|
|
40
|
+
this.row = row
|
|
41
|
+
this.pixels = new Uint32Array(CELL_W * CELL_H)
|
|
42
|
+
this.reset()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
reset() {
|
|
46
|
+
this.char = ' '
|
|
47
|
+
this.fg = DEFAULT_FG
|
|
48
|
+
this.bg = DEFAULT_BG
|
|
49
|
+
this.span = 1
|
|
50
|
+
this.pixels.fill(0)
|
|
51
|
+
this.dirty = true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// A wide character, like an emoji, spans two cells, and the cell after it is left empty.
|
|
55
|
+
writeChar(char, fg = this.fg, span = 1) {
|
|
56
|
+
this.char = char
|
|
57
|
+
this.fg = fg
|
|
58
|
+
this.span = span
|
|
59
|
+
this.dirty = true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setBackground(packed) {
|
|
63
|
+
this.bg = packed
|
|
64
|
+
this.dirty = true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
clearPixels() {
|
|
68
|
+
this.pixels.fill(0)
|
|
69
|
+
this.dirty = true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setPixel(x, y, packed) {
|
|
73
|
+
this.pixels[y * CELL_W + x] = packed
|
|
74
|
+
this.dirty = true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Writes this cell's pixels, with background filled in, into a shared framebuffer.
|
|
78
|
+
compositeInto(buffer, bufferWidth) {
|
|
79
|
+
const originX = this.col * CELL_W
|
|
80
|
+
const originY = this.row * CELL_H
|
|
81
|
+
for (let y = 0; y < CELL_H; y++) {
|
|
82
|
+
const rowStart = (originY + y) * bufferWidth + originX
|
|
83
|
+
for (let x = 0; x < CELL_W; x++) {
|
|
84
|
+
buffer[rowStart + x] = this.pixels[y * CELL_W + x] || this.bg
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
this.dirty = false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
// The grid addresses the same surface two ways: by cell (col, row) for text,
|
|
93
|
+
// and by global pixel (px, py) for graphics. Every pixel belongs to exactly one cell.
|
|
94
|
+
class Grid {
|
|
95
|
+
constructor(cols, rows) {
|
|
96
|
+
this.cols = cols
|
|
97
|
+
this.rows = rows
|
|
98
|
+
this.cells = Array.from({ length: cols * rows }, (_, i) => new Cell(i % cols, Math.floor(i / cols)))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get pixelWidth() { return this.cols * CELL_W }
|
|
102
|
+
get pixelHeight() { return this.rows * CELL_H }
|
|
103
|
+
|
|
104
|
+
cellAt(col, row) {
|
|
105
|
+
if (col < 0 || row < 0 || col >= this.cols || row >= this.rows) return undefined
|
|
106
|
+
return this.cells[row * this.cols + col]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
writeText(col, row, text, fg) {
|
|
110
|
+
;[...text].forEach((char, i) => this.cellAt(col + i, row)?.writeChar(char, fg))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
setPackedPixel(px, py, packed) {
|
|
114
|
+
this.cellAt(Math.floor(px / CELL_W), Math.floor(py / CELL_H))?.setPixel(px % CELL_W, py % CELL_H, packed)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
clear() {
|
|
118
|
+
this.cells.forEach(cell => cell.reset())
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
// Draws a grid in two passes: all cell bitmaps into one low-res framebuffer that is
|
|
124
|
+
// scaled up crisply, then each cell's character centered on top, but for block glyphs, which are drawn as pixels.
|
|
125
|
+
class Renderer {
|
|
126
|
+
constructor(canvas, grid, scale) {
|
|
127
|
+
this.canvas = canvas
|
|
128
|
+
this.grid = grid
|
|
129
|
+
this.scale = scale
|
|
130
|
+
canvas.width = grid.pixelWidth * scale
|
|
131
|
+
canvas.height = grid.pixelHeight * scale
|
|
132
|
+
this.context = canvas.getContext('2d')
|
|
133
|
+
this.context.imageSmoothingEnabled = false
|
|
134
|
+
|
|
135
|
+
this.framebuffer = new OffscreenCanvas(grid.pixelWidth, grid.pixelHeight)
|
|
136
|
+
this.imageData = new ImageData(grid.pixelWidth, grid.pixelHeight)
|
|
137
|
+
this.pixels = new Uint32Array(this.imageData.data.buffer)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
renderDirtyGrid() {
|
|
141
|
+
const dirtyCells = this.grid.cells.filter(cell => cell.dirty)
|
|
142
|
+
if (dirtyCells.length === 0) return
|
|
143
|
+
dirtyCells.forEach(cell => cell.compositeInto(this.pixels, this.grid.pixelWidth))
|
|
144
|
+
this.framebuffer.getContext('2d').putImageData(this.imageData, 0, 0)
|
|
145
|
+
this.context.drawImage(this.framebuffer, 0, 0, this.canvas.width, this.canvas.height)
|
|
146
|
+
this.drawChars()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
drawChars() {
|
|
150
|
+
const cellWidth = CELL_W * this.scale
|
|
151
|
+
const cellHeight = CELL_H * this.scale
|
|
152
|
+
this.context.font = `${Math.floor(cellHeight * 0.85)}px ui-monospace, Menlo, monospace`
|
|
153
|
+
this.context.textAlign = 'center'
|
|
154
|
+
this.context.textBaseline = 'middle'
|
|
155
|
+
for (const cell of this.grid.cells) {
|
|
156
|
+
if (cell.char === ' ' || cell.char === '' || isBlockGlyph(cell.char)) continue
|
|
157
|
+
this.context.fillStyle = cell.fg
|
|
158
|
+
this.context.fillText(cell.char, (cell.col + cell.span / 2) * cellWidth, (cell.row + 0.5) * cellHeight)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
cellAtClientPoint(clientX, clientY) {
|
|
163
|
+
const bounds = this.canvas.getBoundingClientRect()
|
|
164
|
+
const col = Math.floor((clientX - bounds.left) / bounds.width * this.grid.cols)
|
|
165
|
+
const row = Math.floor((clientY - bounds.top) / bounds.height * this.grid.rows)
|
|
166
|
+
return this.grid.cellAt(col, row)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Raindown's rain cloud, written by bin/export-cloud from the terminal UI's Cloud. Edit that, rather than this.
|
|
171
|
+
const CLOUD_DRAWING = {"width":50,"rows":19,"cells":[[2,21,"█","#7eaeec"],[2,22,"▓","#a7b7f6"],[2,23,"▓","#bad7fc"],[2,24,"▓","#a1b2f7"],[2,25,"▓","#86d1f4"],[2,26,"▐","#689ee3"],[2,27,"█","#64bef2"],[2,28,"▓","#7481fb"],[2,29,"▓","#59c0f8"],[3,19,"█","#b5dcf7"],[3,20,"▚","#86b1ea"],[3,21,"▓","#869cfd"],[3,22,"▓","#98abf5"],[3,23,"█","#a2d9f1"],[3,24,"█","#83caf6"],[3,25,"▓","#bfc8fd"],[3,26,"█","#aad9fe"],[3,27,"▓","#99bef5"],[3,28,"█","#859ff4"],[3,29,"▓","#5995fc"],[3,30,"▓","#bcdef5"],[3,31,"▓","#b0dff3"],[4,17,"█","#83aaec"],[4,18,"▓","#5292e0"],[4,19,"█","#66bce1"],[4,20,"▓","#8da9e8"],[4,21,"█","#b3c5ef"],[4,22,"█","#537fdf"],[4,23,"█","#96a4f8"],[4,24,"█","#5371ea"],[4,25,"▓","#478bf0"],[4,26,"▓","#8b9fee"],[4,27,"█","#9ec1fa"],[4,28,"▓","#5198fb"],[4,29,"█","#7dd6fc"],[4,30,"▓","#a1b4f2"],[4,31,"█","#6089e1"],[4,32,"█","#6a9ce7"],[4,33,"█","#51c5fb"],[5,16,"▓","#5f70ec"],[5,17,"█","#5f79ec"],[5,18,"█","#4f69ee"],[5,19,"▓","#8fadf0"],[5,20,"█","#98b9f0"],[5,21,"▓","#a0b0f8"],[5,22,"█","#78b4fc"],[5,23,"▓","#75a8fa"],[5,24,"▓","#8094f9"],[5,25,"▓","#71b3fe"],[5,26,"█","#71b9ea"],[5,27,"█","#8bb3f9"],[5,28,"█","#73aef7"],[5,29,"▓","#87c1f8"],[5,30,"▓","#9ab5ef"],[5,31,"█","#4267fa"],[5,32,"▓","#77b5f3"],[5,33,"▓","#4e5cda"],[5,34,"█","#5274ef"],[6,15,"█","#7ba0e5"],[6,16,"▓","#9fd3f4"],[6,17,"█","#94a6f5"],[6,18,"█","#7fcbeb"],[6,19,"█","#5294e0"],[6,20,"█","#3a98fd"],[6,21,"█","#66c2ff"],[6,22,"▓","#4179e1"],[6,23,"▓","#7fa8e6"],[6,24,"▓","#96a5fd"],[6,25,"█","#779bfd"],[6,26,"▓","#519eec"],[6,27,"█","#4456f8"],[6,28,"▓","#85c8fe"],[6,29,"█","#a1c1f2"],[6,30,"█","#2a54f8"],[6,31,"▓","#5194ec"],[6,32,"█","#6ca4e0"],[6,33,"█","#a1c2ed"],[6,34,"█","#58c9fd"],[6,35,"█","#88d7fc"],[6,37,"█","#6076f0"],[7,15,"█","#5892e9"],[7,16,"▓","#7ea1f1"],[7,17,"█","#63bcf8"],[7,18,"█","#3448fe"],[7,19,"█","#6dcdfd"],[7,20,"▓","#1eb8fa"],[7,21,"▓","#1b87f3"],[7,22,"█","#45afed"],[7,23,"▓","#8dabe8"],[7,24,"▌","#626ffd"],[7,25,"█","#5d88e5"],[7,26,"█","#7eacec"],[7,27,"█","#3e89f9"],[7,28,"█","#8ac6f9"],[7,29,"█","#70b2f0"],[7,30,"█","#6ec5ed"],[7,31,"█","#7696ef"],[7,32,"█","#70c3fa"],[7,33,"█","#8ec6eb"],[7,34,"█","#6383ee"],[7,35,"█","#297bff"],[7,36,"▓","#6894ee"],[7,37,"█","#78a7ed"],[7,38,"█","#8ac0ff"],[7,39,"█","#97acf2"],[7,40,"█","#8ea0fa"],[7,41,"▓","#879ced"],[7,42,"█","#2e49e0"],[8,10,"█","#7aa9e6"],[8,11,"█","#7e88e7"],[8,12,"█","#54b4fc"],[8,13,"▓","#5785e0"],[8,14,"█","#143af5"],[8,15,"█","#89c9eb"],[8,16,"█","#829bf2"],[8,17,"█","#4db8ff"],[8,18,"█","#8997ec"],[8,19,"▀","#4f5ff3"],[8,20,"█","#5a85e2"],[8,21,"█","#428eff"],[8,22,"█","#66a9fa"],[8,23,"█","#2151e4"],[8,24,"█","#134af1"],[8,25,"▓","#7faef0"],[8,26,"█","#86a3f9"],[8,27,"█","#4aa0de"],[8,28,"█","#2a77f4"],[8,29,"▓","#3b62ed"],[8,30,"█","#537fee"],[8,31,"█","#4799e1"],[8,32,"█","#1d38ed"],[8,33,"█","#38b6fa"],[8,34,"█","#43a8db"],[8,35,"█","#70b9e6"],[8,36,"█","#567ef5"],[8,37,"█","#6874fd"],[8,38,"█","#6fa3e7"],[8,39,"█","#798fe6"],[8,40,"█","#7683f4"],[8,41,"█","#3aa8e4"],[8,42,"█","#6f87f1"],[8,43,"▄","#3647fc"],[9,8,"█","#3756e1"],[9,9,"█","#489cea"],[9,10,"█","#208cdf"],[9,11,"█","#485ae5"],[9,12,"█","#2243d3"],[9,13,"▓","#4674d8"],[9,14,"█","#62a1ef"],[9,15,"█","#4473f3"],[9,16,"█","#6abef6"],[9,17,"█","#5e91e8"],[9,18,"█","#4165f6"],[9,19,"█","#2d9efb"],[9,20,"█","#2a5dea"],[9,21,"█","#3376eb"],[9,22,"█","#106df9"],[9,23,"█","#76c9f9"],[9,24,"█","#3b57e3"],[9,25,"█","#768af9"],[9,26,"█","#0e9ef1"],[9,27,"█","#248cd6"],[9,28,"█","#6574ec"],[9,29,"█","#3654fc"],[9,30,"█","#66b2f0"],[9,31,"█","#2e5cdc"],[9,32,"█","#0ea6ec"],[9,33,"█","#5e85e8"],[9,34,"█","#04adf6"],[9,35,"█","#0330fc"],[9,36,"█","#889ae7"],[9,37,"█","#287ff0"],[9,38,"█","#09aefb"],[9,39,"█","#80c7ea"],[9,40,"█","#74c3fb"],[9,41,"█","#196ee6"],[9,42,"█","#528de5"],[9,43,"█","#70a0ff"],[9,44,"█","#768fea"],[10,7,"█","#349edf"],[10,8,"█","#2c7fdd"],[10,9,"█","#6f94f1"],[10,10,"█","#44a3da"],[10,11,"█","#4ec2f4"],[10,12,"█","#5493f2"],[10,13,"█","#62a5df"],[10,14,"█","#58c9fd"],[10,15,"█","#4d5bff"],[10,16,"█","#5fa2dd"],[10,17,"█","#1125df"],[10,18,"█","#172ad9"],[10,19,"█","#3058df"],[10,20,"█","#429eff"],[10,21,"█","#448bfd"],[10,22,"█","#3647fc"],[10,23,"█","#5a66ed"],[10,24,"█","#2335fb"],[10,25,"█","#1e64dc"],[10,26,"█","#4b59f1"],[10,27,"█","#4c99e6"],[10,28,"█","#234cd1"],[10,29,"█","#3153ed"],[10,30,"█","#2b62ee"],[10,31,"█","#697be2"],[10,32,"█","#6abbf0"],[10,33,"█","#0824fd"],[10,34,"█","#4a69d9"],[10,35,"█","#4eaef4"],[10,36,"█","#3c59d7"],[10,37,"█","#3972d5"],[10,38,"█","#4e9ce9"],[10,39,"█","#2438f0"],[10,40,"█","#2333e7"],[10,41,"█","#475ef0"],[10,42,"█","#1f6ee5"],[10,43,"█","#3195d8"],[10,44,"█","#71a1ef"],[10,45,"█","#50b1f1"],[11,6,"█","#1daeed"],[11,7,"█","#1ea3dc"],[11,8,"█","#5777f4"],[11,9,"█","#0f2bff"],[11,10,"█","#1c85e9"],[11,11,"█","#1b8dde"],[11,12,"█","#42aee0"],[11,13,"█","#224afc"],[11,14,"█","#2b95f7"],[11,15,"█","#495efd"],[11,16,"█","#2c90d8"],[11,17,"█","#6081eb"],[11,18,"█","#497bee"],[11,19,"█","#2942d1"],[11,20,"█","#1e7df1"],[11,21,"█","#68ace3"],[11,22,"█","#2270dd"],[11,23,"█","#1c47ca"],[11,24,"█","#4772ff"],[11,25,"█","#0474dc"],[11,26,"█","#2753c4"],[11,27,"█","#0120e9"],[11,28,"█","#619ff5"],[11,29,"█","#6794f4"],[11,30,"█","#1c87ca"],[11,31,"█","#2b70de"],[11,32,"█","#447ee4"],[11,33,"█","#5fb7e3"],[11,34,"█","#0d95de"],[11,35,"█","#5b97e1"],[11,36,"█","#5cb4eb"],[11,37,"█","#366be7"],[11,38,"█","#445ce4"],[11,39,"█","#2c9fd8"],[11,40,"█","#4b63e7"],[11,41,"█","#2a66df"],[11,42,"█","#214ed4"],[11,43,"█","#4891e5"],[11,44,"█","#289deb"],[11,45,"█","#32b8f5"],[12,6,"█","#3853dc"],[12,7,"█","#137fd8"],[12,8,"█","#103bc6"],[12,9,"█","#3887d6"],[12,10,"█","#46a4f6"],[12,11,"█","#1981c2"],[12,12,"█","#34b7ef"],[12,13,"█","#2683b5"],[12,14,"█","#2a6dfe"],[12,15,"█","#37a3eb"],[12,16,"█","#1867c9"],[12,17,"█","#0294e3"],[12,18,"█","#0076d6"],[12,19,"█","#2275ce"],[12,20,"█","#0d59de"],[12,21,"█","#0754da"],[12,22,"█","#45baed"],[12,23,"█","#0394e2"],[12,24,"█","#4e94da"],[12,25,"█","#58b5ee"],[12,26,"█","#5d90ea"],[12,27,"█","#4182d8"],[12,28,"█","#1144ee"],[12,29,"█","#1a51d1"],[12,30,"█","#2aa0e5"],[12,31,"█","#5d8ae5"],[12,32,"█","#43c0fe"],[12,33,"█","#2c60e2"],[12,34,"█","#1978eb"],[12,35,"█","#1d86c3"],[12,36,"█","#2635ba"],[12,37,"█","#5a77ed"],[12,38,"█","#0477fb"],[12,39,"█","#2379af"],[12,40,"█","#143bbd"],[12,41,"█","#3e8ce5"],[12,42,"█","#3f53ee"],[12,43,"█","#44bcf3"],[12,44,"█","#2133d4"],[12,45,"█","#2378c7"],[13,6,"█","#2a43e5"],[13,7,"█","#0440f6"],[13,8,"█","#0494d2"],[13,9,"█","#4fa3e3"],[13,10,"█","#0f7db8"],[13,11,"█","#1d52b4"],[13,12,"█","#5399df"],[13,13,"█","#256bc1"],[13,14,"█","#1b7ff8"],[13,15,"█","#0142da"],[13,16,"█","#116edf"],[13,17,"█","#0379d3"],[13,18,"█","#24a4f9"],[13,19,"█","#3fb1ee"],[13,20,"█","#184ee2"],[13,21,"█","#216dca"],[13,22,"█","#4a59e3"],[13,23,"█","#0f53c7"],[13,24,"█","#6198e0"],[13,25,"█","#4169d8"],[13,26,"█","#1532ef"],[13,27,"█","#074bc0"],[13,28,"█","#3cb7ec"],[13,29,"█","#335cff"],[13,30,"█","#0279e8"],[13,31,"█","#1959b3"],[13,32,"█","#2d90d2"],[13,33,"█","#1288bf"],[13,34,"█","#377ee1"],[13,35,"█","#0475f6"],[13,36,"█","#0b38cb"],[13,37,"█","#1c65e3"],[13,38,"█","#1a79bc"],[13,39,"█","#1a81c7"],[13,40,"█","#1663df"],[13,41,"█","#4155ec"],[13,42,"█","#2030d9"],[13,43,"▌","#094ac3"],[13,44,"█","#4f67f3"],[14,7,"█","#017cef"],[14,8,"█","#0f79eb"],[14,9,"█","#0659f4"],[14,10,"█","#1a99f9"],[14,11,"█","#2541d0"],[14,12,"█","#2e73ea"],[14,13,"█","#0531c2"],[14,14,"█","#0c32ed"],[14,15,"█","#0b64ea"],[14,16,"█","#174bc4"],[14,17,"█","#0a8ae6"],[14,18,"█","#048ad2"],[14,19,"█","#2952f5"],[14,20,"█","#0d59de"],[14,21,"█","#456ad9"],[14,22,"█","#2349a9"],[14,23,"█","#2339c7"],[14,24,"█","#2a65ef"],[14,25,"█","#3142f6"],[14,26,"█","#236da9"],[14,27,"█","#0873bf"],[14,28,"█","#1e9ff6"],[14,29,"█","#3668dd"],[14,30,"█","#014be9"],[14,31,"█","#1e4afa"],[14,32,"█","#0931b3"],[14,33,"█","#0d89bf"],[14,34,"█","#284de2"],[14,35,"█","#2230c9"],[14,36,"█","#128cce"],[14,37,"█","#45a4ed"],[14,38,"█","#0074c2"],[14,39,"█","#376cf1"],[14,40,"█","#1f5ce0"],[14,41,"█","#2a7ef4"],[14,42,"█","#1338f1"],[14,43,"█","#213eb0"],[15,7,"█","#0da9f8"],[15,8,"█","#1477c2"],[15,9,"█","#2b96de"],[15,10,"█","#0826d4"],[15,11,"█","#4aa8de"],[15,12,"█","#3046e8"],[15,13,"█","#0871d4"],[15,14,"█","#1c5eb5"],[15,15,"█","#233caf"],[15,16,"█","#005fdb"],[15,17,"█","#1441f5"],[15,18,"█","#0e43c8"],[15,19,"█","#1847c9"],[15,20,"▐","#174a96"],[15,21,"█","#3565de"],[15,22,"█","#4059e7"],[15,23,"█","#0566e6"],[15,24,"█","#1678b1"],[15,25,"█","#3f5be9"],[15,26,"█","#0d75de"],[15,27,"█","#1154e4"],[15,28,"█","#1f2eb2"],[15,29,"█","#154ef9"],[15,30,"█","#0158e4"],[15,31,"█","#1790ee"],[15,32,"█","#1d7ab4"],[15,33,"█","#26bcfd"],[15,34,"█","#387ad6"],[15,35,"█","#125bb5"],[15,36,"█","#062ff9"],[15,37,"█","#29a0db"],[15,38,"█","#2a68cb"],[15,39,"█","#2575e4"],[15,40,"█","#053acc"],[15,41,"█","#1736cf"],[15,42,"█","#076cdf"],[15,43,"█","#0f4cbd"],[16,8,"█","#1a2bc7"],[16,9,"█","#112092"],[16,10,"█","#0c489d"],[16,11,"█","#2c6fed"],[16,12,"█","#1f60d1"],[16,13,"█","#1ea6f6"],[16,14,"█","#1b7cac"],[16,15,"█","#0876a6"],[16,16,"█","#0c69ac"],[16,17,"█","#015fb7"],[16,18,"█","#3a5ee4"],[16,19,"█","#0c6f9d"],[16,20,"█","#205e9d"],[16,21,"█","#1644b1"],[16,22,"█","#0c49e4"],[16,23,"█","#2ba2de"],[16,24,"█","#172dd3"],[16,25,"█","#1d7fb4"],[16,26,"█","#0b92d5"],[16,27,"█","#0b8bd5"],[16,28,"█","#1a4899"],[16,29,"█","#0936c8"],[16,30,"█","#0335aa"],[16,31,"█","#10739e"],[16,32,"█","#113e92"],[16,33,"█","#3852e5"],[16,34,"█","#1f86c7"],[16,35,"█","#065ff9"],[16,36,"█","#0738ca"],[16,37,"█","#0b43da"],[16,38,"█","#0920c8"],[16,39,"█","#194c94"],[16,40,"█","#1682c0"],[16,41,"█","#368add"],[16,42,"█","#0c77b0"],[17,9,"█","#1c44ca"],[17,10,"█","#0459f6"],[17,11,"█","#061cc6"],[17,12,"█","#162f88"],[17,13,"█","#035fd8"],[17,14,"█","#0351a5"],[17,15,"█","#182795"],[17,16,"█","#0c60df"],[17,17,"█","#015cb7"],[17,18,"█","#1d46d7"],[17,19,"█","#1b86c0"],[17,20,"█","#101fc6"],[17,21,"█","#057de6"],[17,22,"█","#192d86"],[17,23,"█","#1531d1"],[17,24,"█","#032691"],[17,25,"█","#3170dd"],[17,26,"█","#2464b2"],[17,27,"█","#1c6e97"],[17,28,"█","#1348aa"],[17,29,"█","#3047d9"],[17,30,"█","#1a33c1"],[17,31,"█","#031fab"],[17,32,"█","#102689"],[17,33,"█","#1775b0"],[17,34,"█","#1d5a87"],[17,35,"█","#216eb5"],[17,36,"█","#1068fe"],[17,37,"█","#0f3285"],[17,38,"█","#0c558d"],[17,39,"█","#1636b6"],[17,40,"█","#0a1fd6"],[17,41,"▐","#1190d4"],[18,12,"█","#1e47c2"],[18,13,"█","#0b71a8"],[18,14,"█","#1847bf"],[18,15,"█","#173a9b"],[18,16,"█","#0d59af"],[18,17,"█","#1e358f"],[18,18,"█","#055699"],[18,19,"█","#1c3c9b"],[18,20,"█","#2386d1"],[18,21,"█","#2146ab"],[18,22,"█","#2b8aca"],[18,23,"█","#058ac7"],[18,24,"█","#0e6da0"],[18,25,"█","#0230bb"],[18,26,"█","#0734a6"],[18,27,"█","#0988d7"],[18,28,"█","#0f79d7"],[18,29,"█","#055e99"],[18,30,"█","#192b7b"],[18,31,"█","#196b9f"],[18,32,"█","#0b188e"],[18,33,"█","#1f70c7"],[18,34,"█","#1c429c"],[18,35,"█","#1d87dd"],[18,36,"█","#2145b0"],[18,37,"█","#2651b5"],[18,38,"█","#10238e"]],"rainy":[14,15,16,20,21,25,29,30,32,34,35,38,40,42],"rain":["│","╎","┊","'",","],"rainColors":["#66adff","#5b9ae3","#5087c7","#4474ab","#39618f","#2e4e73","#233b57","#17283b"]}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
// Unicode block elements as which of a cell's pixels they cover, so they tile seamlessly, where a font's glyphs leave
|
|
175
|
+
// gaps between rows.
|
|
176
|
+
const HALF_W = CELL_W / 2
|
|
177
|
+
const HALF_H = CELL_H / 2
|
|
178
|
+
const COVERAGE = {
|
|
179
|
+
'█': () => true,
|
|
180
|
+
'▓': (x, y) => x % 2 === 1 || y % 2 === 1,
|
|
181
|
+
'▀': (x, y) => y < HALF_H,
|
|
182
|
+
'▄': (x, y) => y >= HALF_H,
|
|
183
|
+
'▌': (x) => x < HALF_W,
|
|
184
|
+
// Left eighths, like the ends of chart bars, an eighth of the cell each.
|
|
185
|
+
'▏': (x) => x < 1,
|
|
186
|
+
'▎': (x) => x < 2,
|
|
187
|
+
'▍': (x) => x < 3,
|
|
188
|
+
'▋': (x) => x < 5,
|
|
189
|
+
'▊': (x) => x < 6,
|
|
190
|
+
'▉': (x) => x < 7,
|
|
191
|
+
'▐': (x) => x >= HALF_W,
|
|
192
|
+
'▚': (x, y) => (x < HALF_W) === (y < HALF_H),
|
|
193
|
+
'▞': (x, y) => (x < HALF_W) !== (y < HALF_H)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isBlockGlyph(char) {
|
|
197
|
+
return Object.hasOwn(COVERAGE, char)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function paintBlockGlyph(cell, char, packed) {
|
|
201
|
+
const covers = COVERAGE[char]
|
|
202
|
+
for (let y = 0; y < CELL_H; y++) {
|
|
203
|
+
for (let x = 0; x < CELL_W; x++) {
|
|
204
|
+
if (covers(x, y)) cell.setPixel(x, y, packed)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
// The terminal UI's rain cloud, in the middle of the grid: its blocks painted as pixels, in their own blues, and its
|
|
211
|
+
// rain falling from the same columns, by the same checksums, but as pixel streaks moving down a pixel at a time.
|
|
212
|
+
const TICK_MS = 50
|
|
213
|
+
const RAIN_STEPS = CLOUD_DRAWING.rainColors.length
|
|
214
|
+
const RAIN_COLORS = CLOUD_DRAWING.rainColors.map(packColor)
|
|
215
|
+
const DROP_X = CELL_W / 2
|
|
216
|
+
// Each rain character as the pixel rows of its streak, like │ a long line and , a short one.
|
|
217
|
+
const STREAKS = {
|
|
218
|
+
'│': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
|
|
219
|
+
'╎': [2, 3, 4, 5, 9, 10, 11, 12],
|
|
220
|
+
'┊': [2, 3, 6, 7, 10, 11],
|
|
221
|
+
"'": [2, 3, 4],
|
|
222
|
+
',': [11, 12]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
|
|
226
|
+
let c = n
|
|
227
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
|
|
228
|
+
return c >>> 0
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// Zlib's crc32, as the terminal UI checksums where each drop is.
|
|
232
|
+
function crc32(text) {
|
|
233
|
+
let crc = 0xffffffff
|
|
234
|
+
for (const byte of new TextEncoder().encode(text)) crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8)
|
|
235
|
+
return (crc ^ 0xffffffff) >>> 0
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
class RainCloud {
|
|
239
|
+
constructor(grid) {
|
|
240
|
+
this.grid = grid
|
|
241
|
+
this.left = Math.floor((grid.cols - CLOUD_DRAWING.width) / 2)
|
|
242
|
+
this.top = Math.max(Math.floor((grid.rows - CLOUD_DRAWING.rows) / 3), 0)
|
|
243
|
+
this.rainTop = this.top + CLOUD_DRAWING.rows
|
|
244
|
+
this.rainRows = grid.rows - this.rainTop
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
paintCloud() {
|
|
248
|
+
for (const [row, column, block, color] of CLOUD_DRAWING.cells) {
|
|
249
|
+
const cell = this.grid.cellAt(this.left + column, this.top + row)
|
|
250
|
+
if (cell) paintBlockGlyph(cell, block, packColor(color))
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// The rain at a time in ticks, its fraction moving each drop part of the way to the row below.
|
|
255
|
+
paintRain(ticks) {
|
|
256
|
+
const tick = Math.floor(ticks)
|
|
257
|
+
const offset = Math.floor((ticks - tick) * CELL_H)
|
|
258
|
+
for (const column of CLOUD_DRAWING.rainy) {
|
|
259
|
+
for (let row = this.rainTop; row < this.grid.rows; row++) this.grid.cellAt(this.left + column, row)?.clearPixels()
|
|
260
|
+
for (let below = 0; below < this.rainRows; below++) this.paintDrop(column, below, tick, offset)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// The drop, if there's one, in the column at the row below the cloud at the tick, as the terminal UI's drop is.
|
|
265
|
+
paintDrop(column, below, tick, offset) {
|
|
266
|
+
const step = Math.floor(below * RAIN_STEPS / this.rainRows)
|
|
267
|
+
const drop = crc32(`${column} ${below - tick}`)
|
|
268
|
+
if (drop % 100 >= 90 - step * 9) return
|
|
269
|
+
|
|
270
|
+
const streak = STREAKS[CLOUD_DRAWING.rain[drop % CLOUD_DRAWING.rain.length]]
|
|
271
|
+
const x = (this.left + column) * CELL_W + DROP_X
|
|
272
|
+
const y = (this.rainTop + below) * CELL_H + offset
|
|
273
|
+
for (const row of streak) {
|
|
274
|
+
if (y + row < this.grid.pixelHeight) this.grid.setPackedPixel(x, y + row, RAIN_COLORS[step])
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
// The loading page: the rain cloud filling the window until the app's server answers, then the app.
|
|
281
|
+
|
|
282
|
+
// The editor's scale, so the cloud's cells are the grid's: a grid pixel is a CSS pixel, rounded to a whole number
|
|
283
|
+
// of the screen's.
|
|
284
|
+
const DEVICE_SCALE = Math.max(1, Math.round(devicePixelRatio))
|
|
285
|
+
const CSS_SCALE = DEVICE_SCALE / devicePixelRatio
|
|
286
|
+
const POLL_MS = 200
|
|
287
|
+
|
|
288
|
+
const grid = new Grid(
|
|
289
|
+
Math.max(Math.floor(innerWidth / (CELL_W * CSS_SCALE)), 50),
|
|
290
|
+
Math.max(Math.floor(innerHeight / (CELL_H * CSS_SCALE)), 24)
|
|
291
|
+
)
|
|
292
|
+
const canvas = document.querySelector('canvas')
|
|
293
|
+
const renderer = new Renderer(canvas, grid, DEVICE_SCALE)
|
|
294
|
+
canvas.style.width = `${grid.pixelWidth * CSS_SCALE}px`
|
|
295
|
+
canvas.style.height = `${grid.pixelHeight * CSS_SCALE}px`
|
|
296
|
+
const cloud = new RainCloud(grid)
|
|
297
|
+
cloud.paintCloud()
|
|
298
|
+
|
|
299
|
+
function animateRain(time) {
|
|
300
|
+
cloud.paintRain(time / TICK_MS)
|
|
301
|
+
renderer.renderDirtyGrid()
|
|
302
|
+
requestAnimationFrame(animateRain)
|
|
303
|
+
}
|
|
304
|
+
requestAnimationFrame(animateRain)
|
|
305
|
+
|
|
306
|
+
async function openAppWhenServing(url) {
|
|
307
|
+
for (;;) {
|
|
308
|
+
try {
|
|
309
|
+
await fetch(url, { mode: 'no-cors', cache: 'no-store' })
|
|
310
|
+
return location.replace(url)
|
|
311
|
+
} catch {
|
|
312
|
+
await new Promise(resolve => setTimeout(resolve, POLL_MS))
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// The app's URL is the page's fragment, like loading.html#http://127.0.0.1:4133/, as a data: URL has no query.
|
|
318
|
+
const next = location.hash.slice(1)
|
|
319
|
+
if (next) openAppWhenServing(next)
|
|
320
|
+
}
|
|
321
|
+
</script>
|
|
322
|
+
</body>
|
|
323
|
+
</html>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raintron
|
|
4
|
+
# webview_ruby compiles a native C++ extension against the platform's GUI toolkit
|
|
5
|
+
# (Cocoa+WebKit / GTK3+WebKit2GTK / MSHTML). Missing headers otherwise surface as a
|
|
6
|
+
# cryptic mkmf/rake compile failure deep in `bundle install` output, so check for the
|
|
7
|
+
# actual prerequisite upfront and say plainly what's missing. `raintron install`
|
|
8
|
+
# inserts a two-line `require 'preflight/preflight'; Raintron::Preflight.check!` at
|
|
9
|
+
# the top of the consuming app's Gemfile (not a copy-pasted version of this logic),
|
|
10
|
+
# so improvements here reach every consumer via `bundle update` rather than needing
|
|
11
|
+
# every app's Gemfile hand-edited again.
|
|
12
|
+
module Preflight
|
|
13
|
+
def self.check!
|
|
14
|
+
case RbConfig::CONFIG['host_os']
|
|
15
|
+
when /darwin/ then check_macos
|
|
16
|
+
when /linux/ then check_linux
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.check_macos
|
|
21
|
+
return if system('xcrun --find clang++', out: File::NULL, err: File::NULL)
|
|
22
|
+
|
|
23
|
+
warn <<~MSG
|
|
24
|
+
warning: webview_ruby needs the Xcode Command Line Tools to compile its native
|
|
25
|
+
extension (Cocoa/WebKit bindings). Install them with:
|
|
26
|
+
xcode-select --install
|
|
27
|
+
MSG
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.check_linux
|
|
31
|
+
return if system('pkg-config --exists gtk+-3.0 webkit2gtk-4.0', out: File::NULL, err: File::NULL)
|
|
32
|
+
|
|
33
|
+
warn <<~MSG
|
|
34
|
+
warning: webview_ruby needs GTK3 + WebKit2GTK development headers to compile its
|
|
35
|
+
native extension. Install them first, e.g.:
|
|
36
|
+
Debian/Ubuntu: sudo apt-get install -y build-essential libgtk-3-dev libwebkit2gtk-4.0-dev
|
|
37
|
+
Fedora: sudo dnf install -y gcc-c++ gtk3-devel webkit2gtk3-devel
|
|
38
|
+
MSG
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private_class_method :check_macos, :check_linux
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/raintron.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'version'
|
|
4
|
+
require_relative 'launcher/launcher'
|
|
5
|
+
require_relative 'shims/shims'
|
|
6
|
+
require_relative 'icons/icon_generator'
|
|
7
|
+
require_relative 'app_config/app_config'
|
|
8
|
+
|
|
9
|
+
# Merely requiring 'raintron' (i.e. having it in a Gemfile) applies zero shims and
|
|
10
|
+
# never requires 'webview_ruby' -- both only happen inside Launcher#run, and the
|
|
11
|
+
# shims only inside the forked server child. A plain `rain server` dev workflow with
|
|
12
|
+
# raintron in the Gemfile is unaffected by its presence; see lib/shims/shims.rb.
|
|
13
|
+
module Raintron
|
|
14
|
+
class Error < StandardError; end
|
|
15
|
+
class UnsupportedSourceError < Error; end
|
|
16
|
+
|
|
17
|
+
def self.launch(**kwargs, &server_block)
|
|
18
|
+
Launcher.new(**kwargs, &server_block).run
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.packed?
|
|
22
|
+
Shims.packed?
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'io/console'
|
|
4
|
+
|
|
5
|
+
# raindeer's LowFrame calls IO.console.winsize unconditionally to size its terminal
|
|
6
|
+
# animation. IO.console is nil with no controlling tty (e.g. launched from Finder/Dock
|
|
7
|
+
# as a .app, or spawned by webview's bin/desktop), which crashes boot. Fall back to a
|
|
8
|
+
# fixed size in that case; a real terminal's console is used untouched otherwise.
|
|
9
|
+
module Raintron
|
|
10
|
+
module ConsoleShim
|
|
11
|
+
FakeConsole = Struct.new(:winsize)
|
|
12
|
+
|
|
13
|
+
def console(*args)
|
|
14
|
+
super || FakeConsole.new([40, 120])
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
IO.singleton_class.prepend(Raintron::ConsoleShim)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Tebako's packed virtual filesystem interposes regular file I/O (open/read/stat) but
|
|
4
|
+
# not chdir(2) -- gems that Dir.chdir into their own lib directory just to Dir.glob
|
|
5
|
+
# relative to it (e.g. low_type's adapter discovery) get ENOENT for a path that
|
|
6
|
+
# plainly exists. Fall back to running the block without actually changing directory:
|
|
7
|
+
# every current caller of this pattern only globs for optional plugin files that don't
|
|
8
|
+
# ship anyway, so the glob coming up empty either way is a no-op, not a behavior change.
|
|
9
|
+
module Raintron
|
|
10
|
+
module ChdirFallback
|
|
11
|
+
def chdir(dir = nil, &block)
|
|
12
|
+
return super unless block
|
|
13
|
+
|
|
14
|
+
super
|
|
15
|
+
rescue Errno::ENOENT
|
|
16
|
+
block.call(dir)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Dir.singleton_class.prepend(Raintron::ChdirFallback)
|
|
22
|
+
|
|
23
|
+
# Prism.parse_file reads the target file natively (C extension), bypassing whatever
|
|
24
|
+
# path Tebako interposes for Ruby-level File/require reads -- it ENOENTs on packed
|
|
25
|
+
# paths that `require`/`require_relative` load from just fine elsewhere in the same
|
|
26
|
+
# process. low_type's `lowkey` uses it to introspect a gem's own source at load time.
|
|
27
|
+
# Fall back to reading through Ruby's File (which the packed image does support) and
|
|
28
|
+
# parsing that string instead; Prism.parse and Prism.parse_file return the same
|
|
29
|
+
# Prism::ParseResult shape for identical source, so this is a transparent substitution.
|
|
30
|
+
require 'prism'
|
|
31
|
+
|
|
32
|
+
module Raintron
|
|
33
|
+
module PrismParseFileFallback
|
|
34
|
+
def parse_file(filepath, **options)
|
|
35
|
+
super
|
|
36
|
+
rescue Errno::ENOENT
|
|
37
|
+
parse(File.binread(filepath), **options)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
Prism.singleton_class.prepend(Raintron::PrismParseFileFallback)
|
|
43
|
+
|
|
44
|
+
# Tebako doesn't support chdir(2) into any packed path, including the app's own root
|
|
45
|
+
# (real chdir there ENOENTs same as anywhere else) -- but raindeer's boot.rb builds
|
|
46
|
+
# config/config.yaml's path from Dir.pwd, assuming cwd == app root. Rather than actually
|
|
47
|
+
# changing directory, report the app root as Dir.pwd's answer whenever the entry point
|
|
48
|
+
# has told us (via RAINTRON_APP_ROOT) what it is; regular file reads against that path
|
|
49
|
+
# already work fine (require/require_relative use them throughout), so this makes
|
|
50
|
+
# Dir.pwd-relative path math resolve correctly without needing real chdir at all.
|
|
51
|
+
#
|
|
52
|
+
# Launcher sets RAINTRON_APP_ROOT unconditionally, in both packed and normal dev runs
|
|
53
|
+
# (it's simply "what is the app's own root", not a packed-only concept -- it also makes
|
|
54
|
+
# bin/desktop robust to being launched from a cwd other than the project root even
|
|
55
|
+
# without Tebako involved). Because of that, this override can't double as the signal
|
|
56
|
+
# for Shims.packed? the way an earlier draft assumed -- see shims.rb.
|
|
57
|
+
module Raintron
|
|
58
|
+
module PwdOverride
|
|
59
|
+
def pwd
|
|
60
|
+
ENV['RAINTRON_APP_ROOT'] || super
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
Dir.singleton_class.prepend(Raintron::PwdOverride)
|
data/lib/shims/shims.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raintron
|
|
4
|
+
# Applies the environment-compatibility patches a desktop-launched server needs.
|
|
5
|
+
# Never applied by a plain `require 'raintron'` -- only Launcher#start_server calls
|
|
6
|
+
# this, and only inside the forked server child, so a normal `rain server` dev
|
|
7
|
+
# workflow with raintron in the Gemfile is completely unaffected by its presence.
|
|
8
|
+
#
|
|
9
|
+
# Even the packed shims are individually inert on an ordinary filesystem (the chdir
|
|
10
|
+
# and Prism fallbacks only fire on ENOENT; PwdOverride only overrides Dir.pwd when
|
|
11
|
+
# TEBAKO_APP_ROOT is set), so the `packed:` gate here is belt-and-suspenders on top
|
|
12
|
+
# of that, not the only thing preventing interference with normal development.
|
|
13
|
+
module Shims
|
|
14
|
+
def self.apply!(packed: packed?)
|
|
15
|
+
require_relative 'console_shim'
|
|
16
|
+
require_relative 'packed_fs_shim' if packed
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Tebako doesn't document any Ruby-level "am I packed" marker (checked directly
|
|
20
|
+
# against its README/spec docs) -- but every file loaded from inside a packed image,
|
|
21
|
+
# gem code included, resolves to a path containing "/__tfs__/" (confirmed directly
|
|
22
|
+
# against this session's own crash logs from a real pressed build, e.g.
|
|
23
|
+
# ".../__tfs__/lib/ruby/gems/.../raindeer-0.9.9/lib/raindeer/boot.rb"). Checking
|
|
24
|
+
# THIS file's own __dir__ is self-contained -- it doesn't depend on the consuming
|
|
25
|
+
# app's entry point cooperating -- but it is pinned to a specific Tebako version's
|
|
26
|
+
# internal mount name, not a documented stable API, so RAINTRON_FORCE_PACKED is an
|
|
27
|
+
# escape hatch if a future Tebako release ever changes it.
|
|
28
|
+
def self.packed?
|
|
29
|
+
return true if ENV['RAINTRON_FORCE_PACKED']
|
|
30
|
+
|
|
31
|
+
__dir__.include?('/__tfs__/')
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/version.rb
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ffi'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Raintron
|
|
7
|
+
# A window without a title bar, like Obsidian's, on macOS: the page runs under a transparent title bar with its
|
|
8
|
+
# title hidden, leaving only the traffic lights over it. The page takes the title bar's place, so it's told with
|
|
9
|
+
# window.raintron = { titlebar: 'hidden' }, and calls window.raintronDragWindow() on a mouse down where the window
|
|
10
|
+
# should be dragged from, as WebKit takes the mouse events the title bar had.
|
|
11
|
+
#
|
|
12
|
+
# Required only by Launcher#open_window, after webview_ruby, in the window's process. Elsewhere the title bar stays.
|
|
13
|
+
module WindowChrome
|
|
14
|
+
FULL_SIZE_CONTENT_VIEW = 1 << 15
|
|
15
|
+
TITLE_HIDDEN = 1
|
|
16
|
+
|
|
17
|
+
# The Objective-C runtime, with objc_msgSend typed for each shape of message sent here.
|
|
18
|
+
module ObjC
|
|
19
|
+
extend FFI::Library
|
|
20
|
+
|
|
21
|
+
ffi_lib '/usr/lib/libobjc.A.dylib'
|
|
22
|
+
attach_function :get_class, :objc_getClass, [:string], :pointer
|
|
23
|
+
attach_function :selector, :sel_registerName, [:string], :pointer
|
|
24
|
+
attach_function :send_for_pointer, :objc_msgSend, %i[pointer pointer], :pointer
|
|
25
|
+
attach_function :send_for_ulong, :objc_msgSend, %i[pointer pointer], :ulong
|
|
26
|
+
attach_function :send_pointer, :objc_msgSend, %i[pointer pointer pointer], :void
|
|
27
|
+
attach_function :send_ulong, :objc_msgSend, %i[pointer pointer ulong], :void
|
|
28
|
+
attach_function :send_bool, :objc_msgSend, %i[pointer pointer bool], :void
|
|
29
|
+
attach_function :send_long, :objc_msgSend, %i[pointer pointer long], :void
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
def hide_titlebar(webview)
|
|
35
|
+
return unless RbConfig::CONFIG['host_os'].include?('darwin')
|
|
36
|
+
|
|
37
|
+
window = native_window(webview)
|
|
38
|
+
style = ObjC.send_for_ulong(window, ObjC.selector('styleMask'))
|
|
39
|
+
ObjC.send_ulong(window, ObjC.selector('setStyleMask:'), style | FULL_SIZE_CONTENT_VIEW)
|
|
40
|
+
ObjC.send_bool(window, ObjC.selector('setTitlebarAppearsTransparent:'), true)
|
|
41
|
+
ObjC.send_long(window, ObjC.selector('setTitleVisibility:'), TITLE_HIDDEN)
|
|
42
|
+
webview.bind('raintronDragWindow') { |*| drag(window) }
|
|
43
|
+
webview.init("window.raintron = #{JSON.generate(titlebar: 'hidden')}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Drags the window with the mouse down that's being handled, as a title bar would.
|
|
47
|
+
def drag(window)
|
|
48
|
+
application = ObjC.send_for_pointer(ObjC.get_class('NSApplication'), ObjC.selector('sharedApplication'))
|
|
49
|
+
event = ObjC.send_for_pointer(application, ObjC.selector('currentEvent'))
|
|
50
|
+
ObjC.send_pointer(window, ObjC.selector('performWindowDragWithEvent:'), event) unless event.null?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The NSWindow. webview_ruby keeps webview.h's handle to itself and doesn't bind webview_get_window, which its
|
|
54
|
+
# library exports, so it's bound here.
|
|
55
|
+
def native_window(webview)
|
|
56
|
+
WebviewRuby.attach_function(:webview_get_window, [:pointer], :pointer) unless WebviewRuby.respond_to?(:webview_get_window)
|
|
57
|
+
WebviewRuby.webview_get_window(webview.instance_variable_get(:@window))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleName</key>
|
|
6
|
+
<string><%= app_name %></string>
|
|
7
|
+
<key>CFBundleDisplayName</key>
|
|
8
|
+
<string><%= app_name %></string>
|
|
9
|
+
<key>CFBundleIdentifier</key>
|
|
10
|
+
<string><%= bundle_id %></string>
|
|
11
|
+
<key>CFBundleVersion</key>
|
|
12
|
+
<string>1.0</string>
|
|
13
|
+
<key>CFBundleShortVersionString</key>
|
|
14
|
+
<string>1.0</string>
|
|
15
|
+
<key>CFBundlePackageType</key>
|
|
16
|
+
<string>APPL</string>
|
|
17
|
+
<key>CFBundleExecutable</key>
|
|
18
|
+
<string>launch</string>
|
|
19
|
+
<key>CFBundleIconFile</key>
|
|
20
|
+
<string>AppIcon</string>
|
|
21
|
+
<key>NSHighResolutionCapable</key>
|
|
22
|
+
<true/>
|
|
23
|
+
</dict>
|
|
24
|
+
</plist>
|