╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 //! start a server with some fake data and then launch 2 //! the TUI. this provides a nice way to test things 3 //! out safely. 4 5 const std = @import("std"); 6 const builtin = @import("builtin"); 7 const hx = @import("haxy"); 8 const srv = hx.serve; 9 const evt = hx.event; 10 const xit = hx.xit; 11 const rp = xit.repo; 12 const ui = hx.ui; 13 14 pub fn main(init: std.process.Init) !void { 15 var gpa: std.heap.DebugAllocator(.{}) = .init; 16 defer _ = gpa.deinit(); 17 const allocator = gpa.allocator(); 18 19 var threaded = std.Io.Threaded.init(allocator, .{ 20 .environ = init.minimal.environ, 21 }); 22 defer threaded.deinit(); 23 const io = threaded.io(); 24 25 const temp_dir_name = "temp-try"; 26 27 // create the temp dir 28 const cwd = std.Io.Dir.cwd(); 29 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 30 if (temp_dir_or_err) |*temp_dir| { 31 temp_dir.close(io); 32 try cwd.deleteTree(io, temp_dir_name); 33 } else |_| {} 34 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 35 defer cwd.deleteTree(io, temp_dir_name) catch {}; 36 defer temp_dir.close(io); 37 38 // write the dev SSH private key so pushes can authenticate against the 39 // matching public key seeded on the admin account below 40 { 41 const priv_key_file = try temp_dir.createFile(io, "key", .{}); 42 defer priv_key_file.close(io); 43 try priv_key_file.writeStreamingAll(io, 44 \\-----BEGIN OPENSSH PRIVATE KEY----- 45 \\b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 46 \\QyNTUxOQAAACCniLPJiaooAWecvOCeAjoJwCSeWxzysvpTNkpYjF22JgAAAJA+7hikPu4Y 47 \\pAAAAAtzc2gtZWQyNTUxOQAAACCniLPJiaooAWecvOCeAjoJwCSeWxzysvpTNkpYjF22Jg 48 \\AAAEDVlopOMnKt/7by/IA8VZvQXUS/O6VLkixOqnnahUdPCKeIs8mJqigBZ5y84J4COgnA 49 \\JJ5bHPKy+lM2SliMXbYmAAAAC3JhZGFyQHJvYXJrAQI= 50 \\-----END OPENSSH PRIVATE KEY----- 51 \\ 52 ); 53 if (.windows != builtin.os.tag) { 54 try priv_key_file.setPermissions(io, @enumFromInt(0o600)); 55 } 56 } 57 58 const cwd_path = try std.process.currentPathAlloc(io, allocator); 59 defer allocator.free(cwd_path); 60 61 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server", "admin" }); 62 defer allocator.free(work_path); 63 64 const Repo = rp.Repo(.xit, evt.admin_repo_opts); 65 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 66 defer repo.deinit(io, allocator); 67 68 try repo.addConfig(io, allocator, .{ .name = "user.name", .value = "haxy" }); 69 try repo.addConfig(io, allocator, .{ .name = "user.email", .value = "admin@haxy" }); 70 71 var session_arena = std.heap.ArenaAllocator.init(allocator); 72 defer session_arena.deinit(); 73 74 var session: ui.Session = blk: { 75 var arena = std.heap.ArenaAllocator.init(allocator); 76 defer arena.deinit(); 77 78 // define test events 79 80 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 81 82 const user_data = [_]struct { 83 name: []const u8, 84 display_name: []const u8, 85 email: []const u8, 86 }{ 87 .{ .name = "admin", .display_name = "Admin", .email = "admin@example.test" }, 88 .{ .name = "alice", .display_name = "Alice Tulley", .email = "alice@example.test" }, 89 .{ .name = "bob", .display_name = "Bob Smith", .email = "bob@example.test" }, 90 .{ .name = "carol", .display_name = "Carol Johnson", .email = "carol@example.test" }, 91 .{ .name = "dave", .display_name = "Dave Wilson", .email = "dave@example.test" }, 92 .{ .name = "eve", .display_name = "Eve Anderson", .email = "eve@example.test" }, 93 .{ .name = "frank", .display_name = "Frank Miller", .email = "frank@example.test" }, 94 .{ .name = "grace", .display_name = "Grace Lee", .email = "grace@example.test" }, 95 .{ .name = "henry", .display_name = "Henry Davis", .email = "henry@example.test" }, 96 .{ .name = "ivy", .display_name = "Ivy Martinez", .email = "ivy@example.test" }, 97 .{ .name = "jack", .display_name = "Jack Thompson", .email = "jack@example.test" }, 98 .{ .name = "kate", .display_name = "Kate Robinson", .email = "kate@example.test" }, 99 .{ .name = "liam", .display_name = "Liam Walker", .email = "liam@example.test" }, 100 .{ .name = "mona", .display_name = "Mona Patel", .email = "mona@example.test" }, 101 .{ .name = "noah", .display_name = "Noah Garcia", .email = "noah@example.test" }, 102 .{ .name = "olivia", .display_name = "Olivia Hernandez", .email = "olivia@example.test" }, 103 .{ .name = "peter", .display_name = "Peter Wright", .email = "peter@example.test" }, 104 .{ .name = "quinn", .display_name = "Quinn Foster", .email = "quinn@example.test" }, 105 .{ .name = "rachel", .display_name = "Rachel Bennett", .email = "rachel@example.test" }, 106 .{ .name = "sam", .display_name = "Sam Brooks", .email = "sam@example.test" }, 107 .{ .name = "tina", .display_name = "Tina Cooper", .email = "tina@example.test" }, 108 }; 109 110 const repo_data = [_]struct { 111 user_index: usize, 112 name: []const u8, 113 description: []const u8, 114 }{ 115 .{ .user_index = 0, .name = "ziglings", .description = "Learn the Zig programming language by fixing tiny broken programs" }, 116 .{ .user_index = 1, .name = "linux", .description = "Linux kernel source tree" }, 117 .{ .user_index = 2, .name = "kubernetes", .description = "Production-grade container orchestration" }, 118 .{ .user_index = 3, .name = "react", .description = "A declarative, efficient, and flexible JavaScript library for building user interfaces" }, 119 .{ .user_index = 4, .name = "typescript", .description = "TypeScript is a superset of JavaScript that compiles to clean JavaScript output" }, 120 .{ .user_index = 5, .name = "rust", .description = "Empowering everyone to build reliable and efficient software" }, 121 .{ .user_index = 6, .name = "go", .description = "The Go programming language" }, 122 .{ .user_index = 7, .name = "nodejs", .description = "Node.js JavaScript runtime" }, 123 .{ .user_index = 8, .name = "cpython", .description = "The Python programming language" }, 124 .{ .user_index = 9, .name = "docker", .description = "Container platform for developing, shipping, and running applications" }, 125 .{ .user_index = 0, .name = "vim", .description = "The ubiquitous text editor" }, 126 .{ .user_index = 1, .name = "neovim", .description = "Hyperextensible Vim-based text editor" }, 127 .{ .user_index = 2, .name = "emacs", .description = "GNU Emacs source code mirror" }, 128 .{ .user_index = 3, .name = "tmux", .description = "Terminal multiplexer" }, 129 .{ .user_index = 4, .name = "zsh", .description = "Mirror of the Z shell source code repository" }, 130 .{ .user_index = 5, .name = "git", .description = "Distributed version control system" }, 131 .{ .user_index = 6, .name = "mercurial", .description = "Source-control management tool" }, 132 .{ .user_index = 7, .name = "tensorflow", .description = "An end-to-end open source machine learning platform" }, 133 .{ .user_index = 8, .name = "pytorch", .description = "Tensors and dynamic neural networks in Python with strong GPU acceleration" }, 134 .{ .user_index = 9, .name = "numpy", .description = "The fundamental package for scientific computing with Python" }, 135 .{ .user_index = 0, .name = "pandas", .description = "Flexible and powerful data analysis and manipulation library for Python" }, 136 .{ .user_index = 1, .name = "scikit-learn", .description = "Machine learning in Python" }, 137 .{ .user_index = 2, .name = "nginx", .description = "High performance HTTP server and reverse proxy" }, 138 .{ .user_index = 3, .name = "redis", .description = "In-memory data structure store, used as a database, cache, and message broker" }, 139 .{ .user_index = 4, .name = "postgres", .description = "The world's most advanced open source relational database" }, 140 .{ .user_index = 5, .name = "sqlite", .description = "Self-contained, serverless, zero-configuration SQL database engine" }, 141 .{ .user_index = 6, .name = "mongodb", .description = "The MongoDB Database" }, 142 .{ .user_index = 7, .name = "elasticsearch", .description = "Free and open, distributed, RESTful search engine" }, 143 .{ .user_index = 8, .name = "kafka", .description = "Distributed event streaming platform" }, 144 .{ .user_index = 9, .name = "terraform", .description = "Infrastructure as code tool" }, 145 .{ .user_index = 10, .name = "svelte", .description = "Cybernetically enhanced web apps" }, 146 .{ .user_index = 11, .name = "vue", .description = "The progressive JavaScript framework" }, 147 .{ .user_index = 12, .name = "flask", .description = "The Python micro framework for building web applications" }, 148 .{ .user_index = 13, .name = "django", .description = "The web framework for perfectionists with deadlines" }, 149 .{ .user_index = 14, .name = "rails", .description = "Ruby on Rails web framework" }, 150 .{ .user_index = 15, .name = "phoenix", .description = "Peace of mind from prototype to production for Elixir web apps" }, 151 .{ .user_index = 16, .name = "laravel", .description = "The PHP framework for web artisans" }, 152 .{ .user_index = 17, .name = "prometheus", .description = "The Prometheus monitoring system and time series database" }, 153 .{ .user_index = 18, .name = "grafana", .description = "The open and composable observability and data visualization platform" }, 154 .{ .user_index = 19, .name = "ansible", .description = "Simple, agentless IT automation" }, 155 }; 156 157 var user_ids: [user_data.len][evt.event_id_size]u8 = undefined; 158 for (&user_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 159 160 var repo_event_ids: [repo_data.len][evt.event_id_size]u8 = undefined; 161 for (&repo_event_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 162 163 var password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 164 const password_hash = try evt.User.hashPassword("password", &password_hash_buf, io); 165 166 // public key matching temp-try/key, given to admin so we can push as admin 167 const admin_ssh_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeIs8mJqigBZ5y84J4COgnAJJ5bHPKy+lM2SliMXbYm radar@roark"; 168 169 var events_to_consume: [user_data.len + repo_data.len]evt.EventWithId = undefined; 170 for (user_data, 0..) |u, i| { 171 events_to_consume[i] = .{ 172 .id = std.fmt.bytesToHex(user_ids[i], .lower), 173 .event = .{ 174 .user = .{ 175 .name = u.name, 176 .display_name = u.display_name, 177 .email = u.email, 178 .password_hash = password_hash, 179 .ssh_keys = if (i == 0) admin_ssh_key else "", 180 }, 181 }, 182 }; 183 } 184 for (repo_data, 0..) |r, i| { 185 events_to_consume[user_data.len + i] = .{ 186 .id = std.fmt.bytesToHex(repo_event_ids[i], .lower), 187 .event = .{ 188 .repo = .{ 189 .user_id = &user_ids[r.user_index], 190 .name = r.name, 191 .description = r.description, 192 .enable_issue = true, 193 }, 194 }, 195 }; 196 } 197 198 // commit the seed events and consume them into the database 199 try evt.commitAndConsume(evt.admin_repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume); 200 201 // create the actual repos on disk, named by their hex-encoded event id 202 for (repo_data, 0..) |r, i| { 203 const repo_id = std.fmt.bytesToHex(repo_event_ids[i], .lower); 204 const repo_path = try std.fs.path.join(arena.allocator(), &.{ cwd_path, temp_dir_name, "server", "repos", &repo_id }); 205 206 var repo_i = try rp.Repo(.xit, .{}).init(io, allocator, .{ .path = repo_path }); 207 defer repo_i.deinit(io, allocator); 208 209 try repo_i.addConfig(io, allocator, .{ .name = "user.name", .value = "haxy" }); 210 try repo_i.addConfig(io, allocator, .{ .name = "user.email", .value = "admin@haxy" }); 211 212 // write the repo's name and description into a README, plus a 213 // nested doc so the file tree has a directory to descend into 214 { 215 var repo_dir = try cwd.openDir(io, repo_path, .{}); 216 defer repo_dir.close(io); 217 218 const readme = try repo_dir.createFile(io, "README.md", .{}); 219 defer readme.close(io); 220 const readme_content = try std.fmt.allocPrint(arena.allocator(), "# {s}\n\n{s}\n", .{ r.name, r.description }); 221 try readme.writeStreamingAll(io, readme_content); 222 223 try repo_dir.createDirPath(io, "docs/dev"); 224 const doc = try repo_dir.createFile(io, "docs/dev/contribute.md", .{}); 225 defer doc.close(io); 226 try doc.writeStreamingAll(io, "To contribute, please make a pull request"); 227 } 228 229 try repo_i.add(io, allocator, &.{ "README.md", "docs/dev/contribute.md" }); 230 _ = try repo_i.commit(io, allocator, .{ .message = "let there be light" }); 231 232 // a batch of empty commits so the commits tab has more than one page 233 // to paginate through. stepped timestamps vary the date column. 234 const base_ts: u64 = 1_700_000_000; // 2023-11-14 235 var c: usize = 0; 236 while (c < 30) : (c += 1) { 237 const message = try std.fmt.allocPrint(arena.allocator(), "empty commit {d}", .{c + 1}); 238 _ = try repo_i.commit(io, allocator, .{ .message = message, .allow_empty = true, .timestamp = base_ts + c * std.time.s_per_day }); 239 } 240 } 241 242 break :blk try ui.Session.init(&session_arena, &repo, .{}); 243 }; 244 session.is_terminal = true; 245 246 // start the server 247 248 var cli = false; 249 250 var arg_it = try init.minimal.args.iterateAllocator(allocator); 251 defer arg_it.deinit(); 252 _ = arg_it.skip(); 253 while (arg_it.next()) |arg| { 254 if (std.mem.eql(u8, "--cli", arg)) { 255 cli = true; 256 } 257 } 258 259 const server_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server" }); 260 defer allocator.free(server_path); 261 262 // let the native TUI's page builders open the on-disk repos for the file tree 263 session.io = io; 264 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 265 266 if (cli) { 267 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 268 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 269 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 270 271 const key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 272 defer allocator.free(key_path); 273 274 const Runnable = struct { 275 io: std.Io, 276 key_path: []const u8, 277 278 pub fn run(self: @This()) !void { 279 std.debug.print( 280 \\ 281 \\connect to the TUI with: 282 \\ ssh -p 8022 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 283 \\ 284 \\create a git repo and push over SSH: 285 \\ mkdir -p temp-try/client/test 286 \\ cd temp-try/client/test 287 \\ git init 288 \\ echo "hello" > hello.txt 289 \\ git add hello.txt 290 \\ git commit -m "let there be light" 291 \\ GIT_SSH_COMMAND='ssh -p 8022 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -i {s}' git push localhost:admin/test HEAD:master 292 \\ 293 \\to quit, press enter. 294 \\ 295 , .{self.key_path}); 296 // portable stdin read via std.Io — equivalent to the 297 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 298 // on windows. 299 var buf: [1]u8 = undefined; 300 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 301 _ = stdin_reader.interface.takeByte() catch {}; 302 } 303 }; 304 305 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 306 .data_dir = server_path, 307 }, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 308 } else { 309 var null_writer = std.Io.Writer.Discarding.init(&.{}); 310 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 311 312 const Runnable = struct { 313 io: std.Io, 314 allocator: std.mem.Allocator, 315 session: *ui.Session, 316 317 pub fn run(self: @This()) !void { 318 // launch the TUI 319 try hx.ui.run(self.io, self.allocator, self.session); 320 } 321 }; 322 323 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 324 .data_dir = server_path, 325 }, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session }); 326 } 327 } 328