Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions crates/ticgit/src/commands/serve/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
//! title, tags) plus a per-ticket detail page, served over plain HTTP
//! from a hand-rolled `std::net` listener so we pull in no web stack.
//!
//! The ticket pages live in [`tickets`]; this module owns the listener,
//! the request/response plumbing, and the shared page chrome.
//! The ticket pages live in [`tickets`] and the writeup pages in
//! [`writeups`]; this module owns the listener, the request/response
//! plumbing, and the shared page chrome both use.

mod tickets;
mod writeups;

use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
Expand Down Expand Up @@ -213,11 +215,16 @@ fn route(request: &Request) -> Result<Response> {
match request.path.as_str() {
"/" => tickets::list_response(request),
"/tickets.json" => tickets::json_response(request),
"/writeups" => writeups::list_response(request),
"/writeups.json" => writeups::json_response(request),
"/favicon.ico" => Ok(Response::empty(204)),
path => {
if let Some(reference) = path.strip_prefix("/t/").filter(|r| !r.is_empty()) {
return tickets::detail_response(reference);
}
if let Some(reference) = path.strip_prefix("/w/").filter(|r| !r.is_empty()) {
return writeups::detail_response(request, reference);
}
Ok(Response::html(
404,
error_page("404 - not found", "No page at that address."),
Expand Down Expand Up @@ -257,6 +264,16 @@ fn repo_name() -> String {

// -- shared chrome ---------------------------------------------------------

/// A jump to the other half of the site (tickets <-> writeups), set off
/// from the view tabs it sits next to.
fn section_link(href: &str, label: &str) -> String {
format!(
"<a class=\"view section\" href=\"{}\">{}</a>",
escape(href),
escape(label)
)
}

/// Carries the active narrowing through the search form, which would
/// otherwise drop it on submit.
fn hidden_input(name: &str, value: &str) -> String {
Expand Down Expand Up @@ -353,7 +370,16 @@ dd{margin:2px 0 0}\
h2{font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--dim);margin:24px 0 8px}\
.prose{white-space:pre-wrap;word-wrap:break-word;font:inherit;margin:0;\
background:var(--chip);border-radius:6px;padding:12px}\
.comment{margin-bottom:12px}.byline{color:var(--dim);font-size:12px;margin:0 0 4px}";
.comment{margin-bottom:12px}.byline{color:var(--dim);font-size:12px;margin:0 0 4px}\
nav .view.section{color:var(--accent)}\
.links{list-style:none;margin:0;padding:0}\
.links li{padding:5px 0;border-bottom:1px solid var(--line)}\
.links code{color:var(--dim);margin-right:8px}\
.versions{display:flex;gap:4px;flex-wrap:wrap;margin:0 0 10px}\
.vtab{background:var(--chip);color:var(--dim);border-radius:6px;padding:2px 9px;font-size:12px}\
.vtab.active{background:var(--accent);color:#fff}\
td.vers,td.who2{color:var(--dim);white-space:nowrap}\
.state-open{color:#16a34a}.state-closed{color:var(--dim)}";

fn escape(value: &str) -> String {
let mut out = String::with_capacity(value.len());
Expand Down
41 changes: 33 additions & 8 deletions crates/ticgit/src/commands/serve/tickets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
//!
//! The list at `/`, a detail page at `/t/<id>`, and `/tickets.json` for
//! scripting. Page chrome, escaping and the HTTP types all come from the
//! parent module.
//! parent module so both halves of the site look and behave the same.

use anyhow::Result;
use ticgit_lib::{Filter, SearchFilter, SortOrder, Ticket, TicketLifecycle, TicketStatus};
use ticgit_lib::{Filter, SearchFilter, SortOrder, Ticket, TicketLifecycle, TicketStatus, Writeup};
use time::format_description::well_known::Rfc3339;

use super::{
document, error_page, escape, filter_chip, flatten, hidden_input, percent_encode, tag_hue,
Page, Request, Response,
document, error_page, escape, filter_chip, flatten, hidden_input, percent_encode, section_link,
tag_hue, Page, Request, Response,
};
use crate::commands::open_store;
use crate::render;
Expand Down Expand Up @@ -50,7 +50,15 @@ pub(super) fn detail_response(reference: &str) -> Result<Response> {
};
let ticket = store.load(&id)?;
let page = Page::new(&store)?;
Ok(Response::html(200, detail_page(&page, &ticket)))
// Writeups point at tickets, not the other way round, so the back
// link has to come from a scan of the writeup list.
let linked: Vec<Writeup> = store
.list_writeups()
.unwrap_or_default()
.into_iter()
.filter(|writeup| writeup.tickets.contains(&id))
.collect();
Ok(Response::html(200, detail_page(&page, &ticket, &linked)))
}

// -- query -----------------------------------------------------------------
Expand Down Expand Up @@ -358,7 +366,7 @@ fn header(page: &Page, query: &ListQuery) -> String {
),
];
let current = query.href(None);
let nav = views
let mut nav = views
.iter()
.map(|(label, href)| {
format!(
Expand All @@ -369,6 +377,7 @@ fn header(page: &Page, query: &ListQuery) -> String {
})
.collect::<Vec<_>>()
.join("");
nav.push_str(&section_link("/writeups", "Writeups"));

let mut hidden = String::new();
if let Some(status) = &query.status {
Expand Down Expand Up @@ -430,7 +439,7 @@ fn active_filters(query: &ListQuery) -> String {
format!("<div class=\"filters\">{}</div>", chips.join(""))
}

fn detail_page(page: &Page, ticket: &Ticket) -> String {
fn detail_page(page: &Page, ticket: &Ticket, linked_writeups: &[Writeup]) -> String {
let mut body = String::new();
body.push_str(&format!(
"<header class=\"detail\"><a class=\"back\" href=\"/\">\u{2190} all tickets</a>\
Expand Down Expand Up @@ -519,6 +528,22 @@ fn detail_page(page: &Page, ticket: &Ticket) -> String {
escape(spec)
));
}
if !linked_writeups.is_empty() {
body.push_str(&format!(
"<section><h2>Writeups ({})</h2><ul class=\"links\">",
linked_writeups.len()
));
for writeup in linked_writeups {
body.push_str(&format!(
"<li><a href=\"/w/{}\"><code>{}</code> {}</a></li>",
escape(&writeup.short_id()),
escape(&writeup.short_id()),
escape(&flatten(&writeup.title)),
));
}
body.push_str("</ul></section>");
}

if !ticket.comments.is_empty() {
body.push_str(&format!(
"<section><h2>Comments ({})</h2>",
Expand Down Expand Up @@ -707,7 +732,7 @@ mod tests {
at: OffsetDateTime::UNIX_EPOCH,
body: "on it".to_string(),
});
let html = detail_page(&page(), &t);
let html = detail_page(&page(), &t, &[]);
assert!(html.contains("fix parser"));
assert!(html.contains("in-progress"));
assert!(html.contains("a longer\nexplanation"));
Expand Down
Loading