mirror of
http://101.35.51.105:3000/congyu/Hakysidian.git
synced 2026-04-28 05:50:49 +08:00
Compare commits
7 Commits
e419366615
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f0d36015b | |||
| 3a3fd97055 | |||
| aa05d73c9c | |||
| 3652459503 | |||
| fc4cac00d5 | |||
| 55719f3444 | |||
| d4629ec8e7 |
+21
-8
@@ -11,22 +11,22 @@
|
||||
}
|
||||
|
||||
.theorem-header .index:before {
|
||||
content: ' ';
|
||||
content: " ";
|
||||
}
|
||||
|
||||
.theorem-header .name:before {
|
||||
content: ' (';
|
||||
content: " (";
|
||||
}
|
||||
|
||||
.theorem-header .name:after {
|
||||
content: ')';
|
||||
content: ")";
|
||||
}
|
||||
|
||||
.theorem-header:after {
|
||||
content: '.\2002\2002';
|
||||
content: ".\2002\2002";
|
||||
}
|
||||
|
||||
.theorem-header+p {
|
||||
.theorem-header + p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@@ -42,12 +42,25 @@
|
||||
}
|
||||
|
||||
.Proof:after {
|
||||
content: '∎';
|
||||
content: "∎";
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.Proof span.theorem-header span.name {
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.Proof span.theorem-header span.name:before {
|
||||
content: " ";
|
||||
}
|
||||
|
||||
.Proof span.theorem-header span.name:after {
|
||||
content: " ";
|
||||
}
|
||||
|
||||
table.postindex {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -66,7 +79,7 @@ table.postindex td.right {
|
||||
}
|
||||
|
||||
.header-section-number:after {
|
||||
content: '.';
|
||||
content: ".";
|
||||
}
|
||||
|
||||
.csl-entry {
|
||||
@@ -85,6 +98,6 @@ table.postindex td.right {
|
||||
.csl-right-inline {
|
||||
display: table-cell;
|
||||
}
|
||||
.csl-right-inline a{
|
||||
.csl-right-inline a {
|
||||
word-break: break-all;
|
||||
}
|
||||
+66
-119
@@ -3,10 +3,7 @@
|
||||
--color-tag1: gray;
|
||||
--color-tag2: darkolivegreen;
|
||||
--color-bg: white;
|
||||
--color-link: #337ab7;
|
||||
--color-linkhbg: #e6f0ff;
|
||||
--color-linkh: #002266;
|
||||
--color-bq: olivedrab;
|
||||
--color-link: #0000ee;
|
||||
--color-notice: #fb4f4f;
|
||||
}
|
||||
|
||||
@@ -19,11 +16,18 @@
|
||||
|
||||
html {
|
||||
scrollbar-gutter: stable;
|
||||
scroll-behavior: smooth;
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Lato', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
font-family:
|
||||
"Lato",
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"PingFang SC",
|
||||
"Microsoft YaHei",
|
||||
sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
@@ -32,30 +36,28 @@ body {
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg);
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
body.lang-zh {
|
||||
text-align: left;
|
||||
text-autospace: no-autospace; /*using pangu.hs*/
|
||||
}
|
||||
|
||||
body a {
|
||||
color: var(--color-link);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.text-space a:hover {
|
||||
background-color: var(--color-linkhbg);
|
||||
color: var(--color-linkh);
|
||||
text-decoration: none;
|
||||
body a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
details {
|
||||
background-color: var(--color-linkhbg);
|
||||
padding-left: 1em;
|
||||
border: 2px solid var(--color-text);
|
||||
}
|
||||
summary:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*mathML*/
|
||||
.htmlmathparagraph, mtext,math {
|
||||
.htmlmathparagraph,
|
||||
mtext,
|
||||
math {
|
||||
font-family: Lete Sans Math;
|
||||
}
|
||||
.math-container,
|
||||
@@ -63,7 +65,7 @@ summary:hover {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: .5em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.math-container.math-container-tagged {
|
||||
@@ -72,7 +74,7 @@ summary:hover {
|
||||
align-items: center;
|
||||
column-gap: 1rem;
|
||||
overflow: visible;
|
||||
padding: .5em 0;
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
.math-container.math-container-tagged .math-tag-spacer {
|
||||
@@ -83,7 +85,7 @@ summary:hover {
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: .5em 0;
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
.math-container.math-container-tagged .math-tag {
|
||||
@@ -104,66 +106,32 @@ summary:hover {
|
||||
font-variant-caps: small-caps;
|
||||
}
|
||||
|
||||
p {
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
a.url {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
html body div.text-space main ul.post-list {
|
||||
list-style-type: none;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
/* top bar */
|
||||
header {
|
||||
font-weight: 400;
|
||||
font-family: "IosevkaC", sans-serif;
|
||||
}
|
||||
|
||||
/* top bar*/
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navright a {
|
||||
margin: 0 0 0 1em;
|
||||
}
|
||||
|
||||
/* Links inside the navbar */
|
||||
.navbar a {
|
||||
text-decoration: none;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.navbar a:visited {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
nav {
|
||||
text-align: right;
|
||||
border-bottom: solid 1px var(--color-text);
|
||||
}
|
||||
|
||||
nav a {
|
||||
font-size: 1.2rem;
|
||||
/*margin-left: 0.5em;*/
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.uri {
|
||||
word-wrap: break-word;
|
||||
/* Legacy support */
|
||||
overflow-wrap: break-word;
|
||||
/* Modern property */
|
||||
word-break: break-all;
|
||||
/* Break long words if necessary */
|
||||
white-space: normal;
|
||||
/* Allow wrapping */
|
||||
}
|
||||
|
||||
|
||||
|
||||
footer {
|
||||
color: var(--color-text);
|
||||
font-size: 0.8rem;
|
||||
@@ -172,15 +140,6 @@ footer {
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pagetitle {
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
@@ -194,22 +153,20 @@ h1 {
|
||||
font-size: 1.44rem;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
text-align: left;
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 1em;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
font-style: normal
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 1em;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
font-style: normal
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
article .header {
|
||||
@@ -219,14 +176,7 @@ article .header {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
.info {
|
||||
color: var(--color-tag2);
|
||||
font-size: 1rem;
|
||||
font-style: normal;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.info,
|
||||
.info a {
|
||||
color: var(--color-tag2);
|
||||
font-size: 1rem;
|
||||
@@ -247,33 +197,45 @@ section.body {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 1rem 0;
|
||||
padding: 0 0 0 1.5em;
|
||||
border-left: 3px solid var(--color-bq);
|
||||
/* table. copied from https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/table */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border: 2px solid rgb(140 140 140);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
blockquote p {
|
||||
margin: 0;
|
||||
caption {
|
||||
caption-side: bottom;
|
||||
padding: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ol {
|
||||
padding-left: 2em;
|
||||
}
|
||||
ul {
|
||||
list-style-type: square;
|
||||
padding-left: 2em;
|
||||
}
|
||||
li {
|
||||
margin-bottom: 0.15em;
|
||||
thead,
|
||||
tfoot {
|
||||
background-color: rgb(228 240 245);
|
||||
}
|
||||
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border: 1px solid darkolivegreen;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
border: 1px solid rgb(160 160 160);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
td:last-of-type {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
tbody > tr:nth-of-type(even) {
|
||||
background-color: rgb(237 238 242);
|
||||
}
|
||||
|
||||
tfoot th {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
tfoot td {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
figure {
|
||||
@@ -284,23 +246,11 @@ figure {
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
|
||||
figcaption {
|
||||
/* font: italic smaller sans-serif; */
|
||||
padding: 3px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.caption {
|
||||
display: none
|
||||
}
|
||||
|
||||
.centerimg img {
|
||||
margin: 0 auto 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
div.highlight,
|
||||
pre code {
|
||||
margin: auto;
|
||||
@@ -320,18 +270,16 @@ code {
|
||||
text-rendering: optimizeSpeed;
|
||||
}
|
||||
|
||||
|
||||
.draft-notice {
|
||||
color: var(--color-notice);
|
||||
margin: 1em auto;
|
||||
text-align: center
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.subtitle {
|
||||
text-align: left;
|
||||
font-size: 1.2rem;
|
||||
margin-top: 0
|
||||
margin-top: 0;
|
||||
}
|
||||
.gallery {
|
||||
margin-top: 2em;
|
||||
@@ -401,7 +349,7 @@ code {
|
||||
padding-left: 1em;
|
||||
line-height: 1.2;
|
||||
list-style-type: decimal;
|
||||
margin-left: 0
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
div#contents ul.notes-list,
|
||||
@@ -414,8 +362,8 @@ code {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
div#contents-big li+li {
|
||||
margin-top: 0.5em
|
||||
div#contents-big li + li {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
div#contents-big {
|
||||
@@ -428,7 +376,7 @@ code {
|
||||
margin-right: 4em;
|
||||
position: sticky;
|
||||
top: 5rem;
|
||||
left: 100%
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
div#contents-big .mini-header {
|
||||
@@ -452,7 +400,6 @@ code {
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
.no-print,
|
||||
.no-print * {
|
||||
display: none !important;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/* fonts */
|
||||
|
||||
@font-face {
|
||||
|
||||
@@ -33,6 +33,7 @@ executable hakysidian
|
||||
, filepath
|
||||
, process
|
||||
, time
|
||||
, unix
|
||||
, wai-app-static
|
||||
, warp
|
||||
-- , ghc-syntax-highlighter
|
||||
|
||||
@@ -1,6 +1,130 @@
|
||||
things don't work:
|
||||
|
||||
1. equation labels & paragraph labels
|
||||
2. pandoc does not support mathtools: <https://github.com/jgm/texmath/issues/249>
|
||||
3. cross document refs
|
||||
4.
|
||||
# Drawbacks
|
||||
|
||||
- currently all shared files (css, templates, csl files...) are stored in `~/.cabal/store/`. there will be a copy for every compile
|
||||
- web preview needs a port. if you don't set port manually, you cannot preview two projects at the same time.
|
||||
|
||||
|
||||
|
||||
--------
|
||||
|
||||
|
||||
# hakysidian
|
||||
|
||||
`hakysidian` is a static site generator for note projects.
|
||||
|
||||
It is built on Hakyll, but packaged as a reusable CLI so you can run the same site generator across multiple note repositories without copying shared assets around. The executable bundles its shared `css/`, `fonts/`, `templates/`, `favicon.ico`, and `bib_style.csl` files with Cabal, then reads project-specific content from the current working directory.
|
||||
|
||||
## What It Expects
|
||||
|
||||
Run `hakysidian` inside a project directory with this layout:
|
||||
|
||||
```text
|
||||
your-project/
|
||||
├── notes/
|
||||
│ ├── first-note.md
|
||||
│ └── another-note.md
|
||||
├── reference.bib
|
||||
├── math-macros.md
|
||||
└── images/ # optional
|
||||
```
|
||||
|
||||
Required inputs:
|
||||
|
||||
- `notes/`: markdown notes to compile.
|
||||
- `reference.bib`: bibliography used by Pandoc citeproc.
|
||||
- `math-macros.md`: math macro definitions prepended before note parsing.
|
||||
|
||||
Optional inputs:
|
||||
|
||||
- `images/`: copied into the generated site as-is.
|
||||
|
||||
Shared assets are not required in each project. They come from the installed `hakysidian` package.
|
||||
|
||||
## Output
|
||||
|
||||
By default, `hakysidian` writes:
|
||||
|
||||
- `_site/`: generated site output.
|
||||
- `_cache/`: Hakyll cache and temporary files.
|
||||
|
||||
Note pages use clean URLs. For example:
|
||||
|
||||
```text
|
||||
notes/graph.md -> _site/notes/graph/index.html
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
From this repository:
|
||||
|
||||
```bash
|
||||
cabal build exe:hakysidian
|
||||
cabal install exe:hakysidian
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
The default CLI mirrors the common Hakyll workflow:
|
||||
|
||||
```bash
|
||||
hakysidian build
|
||||
hakysidian clean
|
||||
hakysidian rebuild
|
||||
hakysidian watch
|
||||
```
|
||||
|
||||
`watch` also supports:
|
||||
|
||||
```bash
|
||||
hakysidian watch --host 127.0.0.1 --port 8000
|
||||
hakysidian watch --no-server
|
||||
```
|
||||
|
||||
The dashboard is now an explicit TUI mode:
|
||||
|
||||
```bash
|
||||
hakysidian -tui
|
||||
hakysidian -tui --host 127.0.0.1 --port 8000
|
||||
hakysidian -tui --no-server
|
||||
```
|
||||
|
||||
What each command does:
|
||||
|
||||
- `build`: incremental site build.
|
||||
- `clean`: removes generated output and cache.
|
||||
- `rebuild`: clears output/cache and builds from scratch.
|
||||
- `watch`: runs Hakyll's normal watch workflow, prints build logs directly to the terminal, and rebuilds automatically on change.
|
||||
- `-tui`: starts the interactive dashboard with explicit controls for watching and cleaning.
|
||||
|
||||
## Watch And TUI
|
||||
|
||||
Both `watch` and `-tui` work against the same project inputs:
|
||||
|
||||
- `notes/**`
|
||||
- `reference.bib`
|
||||
- `math-macros.md`
|
||||
- `images/**`
|
||||
|
||||
Normal `watch` behaves like a standard Hakyll watch command: it stays in the terminal, rebuilds when inputs change, and can start a preview server unless `--no-server` is passed.
|
||||
|
||||
`-tui` uses an alternate-screen dashboard that:
|
||||
|
||||
- uses the terminal’s current size to keep the dashboard within the visible screen,
|
||||
- keeps recent build output in a bounded activity pane,
|
||||
- can start a local preview server unless `--no-server` is passed,
|
||||
- supports `w` to start watching, `s` to stop watching, `c` to clean, and `q` to quit.
|
||||
|
||||
The TUI requires an interactive terminal.
|
||||
|
||||
## Notes Format
|
||||
|
||||
This generator is opinionated toward the current note pipeline in this repository:
|
||||
|
||||
- Markdown is parsed with Pandoc and custom theorem/callout handling.
|
||||
- math is rendered with MathML. looks good in firefox
|
||||
- sidenotes are supported
|
||||
- spacing between CJK chars and ascii is automatically handled by a filter.
|
||||
- Citations are processed through `reference.bib` and the bundled `bib_style.csl`.
|
||||
- `math-macros.md` is injected before parsing so note content and theorem titles can use the same macros.
|
||||
- Notes are rendered with the bundled templates and stylesheet set.
|
||||
|
||||
+255
-77
@@ -4,13 +4,12 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneKindSignatures #-}
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
|
||||
import ChaoDoc
|
||||
import Control.Concurrent (forkIO, threadDelay)
|
||||
import Control.Exception (SomeException, bracket_, try)
|
||||
import Control.Monad (filterM, unless, void)
|
||||
import Data.Char (isSpace)
|
||||
import Control.Monad (filterM, unless, void, when)
|
||||
import Data.Char (isSpace, toLower)
|
||||
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
|
||||
import Data.Kind (Type)
|
||||
import Data.List (intercalate, isPrefixOf, sort, sortOn)
|
||||
@@ -23,6 +22,7 @@ import Data.Time.Format (defaultTimeLocale, formatTime)
|
||||
import Data.Time.LocalTime (getZonedTime)
|
||||
import Hakyll
|
||||
import Hakyll.Core.Runtime (RunMode (RunModeNormal))
|
||||
import Network.Wai.Application.Static (staticApp)
|
||||
import qualified Network.Wai.Handler.Warp as Warp
|
||||
import qualified Paths_hakysidian as Paths
|
||||
import System.Directory
|
||||
@@ -31,19 +31,32 @@ import System.Directory
|
||||
doesFileExist,
|
||||
getCurrentDirectory,
|
||||
getModificationTime,
|
||||
listDirectory
|
||||
listDirectory,
|
||||
)
|
||||
import System.Environment (getArgs, getExecutablePath, lookupEnv)
|
||||
import System.Exit (ExitCode (..), die, exitSuccess, exitWith)
|
||||
import System.FilePath
|
||||
import Network.Wai.Application.Static (staticApp)
|
||||
import System.IO
|
||||
( BufferMode (NoBuffering),
|
||||
hFlush,
|
||||
hGetBuffering,
|
||||
hGetChar,
|
||||
hIsTerminalDevice,
|
||||
hSetBuffering,
|
||||
stdout
|
||||
hWaitForInput,
|
||||
stdin,
|
||||
stdout,
|
||||
)
|
||||
import System.Posix.IO (stdInput)
|
||||
import System.Posix.Terminal
|
||||
( TerminalAttributes,
|
||||
TerminalMode (EnableEcho, ProcessInput),
|
||||
TerminalState (Immediately),
|
||||
getTerminalAttributes,
|
||||
setTerminalAttributes,
|
||||
withMinInput,
|
||||
withTime,
|
||||
withoutMode,
|
||||
)
|
||||
import System.Process (CreateProcess (cwd), proc, readCreateProcessWithExitCode)
|
||||
import Text.Pandoc (HTMLMathMethod (MathML), WriterOptions (..), compileTemplate)
|
||||
@@ -99,11 +112,24 @@ data CliCommand
|
||||
| CleanCommand
|
||||
| HelpCommand
|
||||
| RebuildCommand
|
||||
| TuiCommand WatchSettings
|
||||
| WatchCommand WatchSettings
|
||||
|
||||
type TuiAction :: Type
|
||||
data TuiAction
|
||||
= TuiClean
|
||||
| TuiQuit
|
||||
| TuiStartWatching
|
||||
| TuiStopWatching
|
||||
|
||||
type FileSnapshot :: Type
|
||||
type FileSnapshot = M.Map FilePath UTCTime
|
||||
|
||||
type TuiWatchState :: Type
|
||||
data TuiWatchState
|
||||
= TuiWatchStopped
|
||||
| TuiWatching FileSnapshot
|
||||
|
||||
type ServerStatus :: Type
|
||||
data ServerStatus
|
||||
= ServerDisabled
|
||||
@@ -190,14 +216,17 @@ main = do
|
||||
Right RebuildCommand -> do
|
||||
validateProject projectRoot
|
||||
exitWith =<< runSiteCommand config rebuildOptions cslPath
|
||||
Right (TuiCommand watchSettings) -> do
|
||||
validateProject projectRoot
|
||||
exitWith =<< runTui projectRoot config cslPath watchSettings
|
||||
Right (WatchCommand watchSettings) -> do
|
||||
validateProject projectRoot
|
||||
exitWith =<< runWatch projectRoot config cslPath watchSettings
|
||||
exitWith =<< runSiteCommand config (watchOptions watchSettings) cslPath
|
||||
|
||||
usageText :: String
|
||||
usageText =
|
||||
unlines
|
||||
[ "usage: hakysidian [build|clean|rebuild|watch [--host HOST] [--port PORT] [--no-server]]",
|
||||
[ "usage: hakysidian [build|clean|rebuild|watch [--host HOST] [--port PORT] [--no-server]|-tui [--host HOST] [--port PORT] [--no-server]]",
|
||||
"",
|
||||
"Run inside a project directory containing notes/, reference.bib, math-macros.md, and optional images/."
|
||||
]
|
||||
@@ -210,6 +239,8 @@ parseCliCommand config args
|
||||
["build"] -> Right BuildCommand
|
||||
["clean"] -> Right CleanCommand
|
||||
["rebuild"] -> Right RebuildCommand
|
||||
"-tui" : rest -> Right (TuiCommand (parseWatchSettings config rest))
|
||||
"--tui" : rest -> Right (TuiCommand (parseWatchSettings config rest))
|
||||
"watch" : rest -> Right (WatchCommand (parseWatchSettings config rest))
|
||||
command : _ -> Left ("Unknown command: " <> command)
|
||||
|
||||
@@ -232,18 +263,18 @@ validateProject projectRoot = do
|
||||
unless (null missing) $
|
||||
die $
|
||||
unlines $
|
||||
"hakysidian is missing required project inputs:" :
|
||||
map (" - " ++) missing
|
||||
"hakysidian is missing required project inputs:"
|
||||
: map (" - " ++) missing
|
||||
|
||||
initialDashboardState :: DashboardState
|
||||
initialDashboardState =
|
||||
DashboardState
|
||||
{ dashboardStatus = "starting",
|
||||
dashboardLastChange = "waiting for first build",
|
||||
dashboardLastBuild = "pending",
|
||||
{ dashboardStatus = "idle",
|
||||
dashboardLastChange = "press w to start watching",
|
||||
dashboardLastBuild = "no command run yet",
|
||||
dashboardLogLines =
|
||||
[ "watcher ready",
|
||||
"watching notes/, reference.bib, math-macros.md, images/ (optional)"
|
||||
[ "tui ready",
|
||||
"controls: w watch, s stop, c clean, q quit"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -289,31 +320,62 @@ extractOptionValue option = go
|
||||
|
||||
withWatchTui :: IO a -> IO a
|
||||
withWatchTui action = do
|
||||
interactive <- hIsTerminalDevice stdout
|
||||
if interactive
|
||||
stdoutInteractive <- hIsTerminalDevice stdout
|
||||
stdinInteractive <- hIsTerminalDevice stdin
|
||||
if stdoutInteractive
|
||||
then do
|
||||
originalBuffering <- hGetBuffering stdout
|
||||
originalInputBuffering <- hGetBuffering stdin
|
||||
originalInputMode <-
|
||||
if stdinInteractive
|
||||
then Just <$> getTerminalAttributes stdInput
|
||||
else pure Nothing
|
||||
bracket_
|
||||
(do
|
||||
( do
|
||||
hSetBuffering stdout NoBuffering
|
||||
when stdinInteractive do
|
||||
hSetBuffering stdin NoBuffering
|
||||
maybe
|
||||
(pure ())
|
||||
(\inputMode -> setTerminalAttributes stdInput (watchInputMode inputMode) Immediately)
|
||||
originalInputMode
|
||||
putStr "\ESC[?1049h\ESC[2J\ESC[H\ESC[?25l"
|
||||
hFlush stdout)
|
||||
(do
|
||||
hFlush stdout
|
||||
)
|
||||
( do
|
||||
putStr "\ESC[0m\ESC[?25h\ESC[?1049l"
|
||||
hFlush stdout
|
||||
hSetBuffering stdout originalBuffering)
|
||||
maybe
|
||||
(pure ())
|
||||
(\inputMode -> setTerminalAttributes stdInput inputMode Immediately)
|
||||
originalInputMode
|
||||
when stdinInteractive do
|
||||
hSetBuffering stdin originalInputBuffering
|
||||
hSetBuffering stdout originalBuffering
|
||||
)
|
||||
action
|
||||
else action
|
||||
|
||||
watchInputMode :: TerminalAttributes -> TerminalAttributes
|
||||
watchInputMode inputMode =
|
||||
withTime
|
||||
( withMinInput
|
||||
( withoutMode
|
||||
(withoutMode inputMode ProcessInput)
|
||||
EnableEcho
|
||||
)
|
||||
1
|
||||
)
|
||||
0
|
||||
|
||||
renderWatchDashboard ::
|
||||
IORef (Maybe (TerminalSize, ServerStatus, DashboardState)) ->
|
||||
FilePath ->
|
||||
Configuration ->
|
||||
WatchSettings ->
|
||||
IORef ServerStatus ->
|
||||
DashboardState ->
|
||||
IO ()
|
||||
renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatusRef dashboard = do
|
||||
renderWatchDashboard renderStateRef projectRoot watchSettings serverStatusRef dashboard = do
|
||||
terminalSize <- getTerminalSize
|
||||
serverStatus <- readIORef serverStatusRef
|
||||
previousRenderState <- readIORef renderStateRef
|
||||
@@ -324,20 +386,22 @@ renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatu
|
||||
border = "+" ++ replicate (cols - 2) '-' ++ "+"
|
||||
infoRows =
|
||||
[ dashboardRow cols ("Project : " ++ projectRoot),
|
||||
dashboardRow cols ("Output : " ++ destinationDirectory config),
|
||||
dashboardRow cols ("Preview : " ++ renderServerStatus watchSettings serverStatus),
|
||||
dashboardRow cols "Watch : notes/, reference.bib, math-macros.md, images/ (optional)",
|
||||
dashboardRow cols ("Change : " ++ dashboardLastChange dashboard),
|
||||
dashboardRow cols ("Build : " ++ dashboardLastBuild dashboard)
|
||||
dashboardRow cols ("Last op : " ++ dashboardLastBuild dashboard)
|
||||
]
|
||||
headerRows =
|
||||
[ border,
|
||||
dashboardTitleRow cols "hakysidian watch" (dashboardStatus dashboard),
|
||||
dashboardTitleRow cols "hakysidian tui" (dashboardStatus dashboard),
|
||||
border
|
||||
]
|
||||
++ infoRows
|
||||
++ [border, dashboardRow cols "Recent activity", border]
|
||||
footerRows = [border]
|
||||
footerRows =
|
||||
[ border,
|
||||
dashboardRow cols "Controls: w watch, s stop, c clean, q quit, Ctrl-C interrupt",
|
||||
border
|
||||
]
|
||||
availableLogRows = max 1 (rows - length headerRows - length footerRows)
|
||||
logRows =
|
||||
map (dashboardRow cols) $
|
||||
@@ -345,13 +409,13 @@ renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatu
|
||||
takeLast availableLogRows (dashboardLogLines dashboard)
|
||||
screenRows = take rows (headerRows ++ logRows ++ footerRows)
|
||||
putStr "\ESC[2J\ESC[H"
|
||||
putStr (unlines screenRows)
|
||||
putStr (intercalate "\n" screenRows)
|
||||
hFlush stdout
|
||||
writeIORef renderStateRef currentRenderState
|
||||
|
||||
dashboardTitleRow :: Int -> String -> String -> String
|
||||
dashboardTitleRow width leftText rightText =
|
||||
dashboardFramedRow width (leftText ++ spacer ++ clippedRight)
|
||||
dashboardRow width (leftText ++ spacer ++ clippedRight)
|
||||
where
|
||||
usableWidth = max 1 (width - 4)
|
||||
rightWidth = min (usableWidth `div` 3) (length rightText)
|
||||
@@ -366,10 +430,7 @@ dashboardTitleRow width leftText rightText =
|
||||
| otherwise = replicate (max 1 (usableWidth - length clippedLeft - length clippedRight)) ' '
|
||||
|
||||
dashboardRow :: Int -> String -> String
|
||||
dashboardRow width = dashboardFramedRow width
|
||||
|
||||
dashboardFramedRow :: Int -> String -> String
|
||||
dashboardFramedRow width content =
|
||||
dashboardRow width content =
|
||||
"| " ++ padRight usableWidth (ellipsize usableWidth content) ++ " |"
|
||||
where
|
||||
usableWidth = max 1 (width - 4)
|
||||
@@ -405,8 +466,8 @@ getTerminalSize = do
|
||||
case sttySize of
|
||||
Just terminalSize -> pure terminalSize
|
||||
Nothing -> do
|
||||
rows <- maybe 24 id . (>>= readMaybe) <$> lookupEnv "LINES"
|
||||
cols <- maybe 80 id . (>>= readMaybe) <$> lookupEnv "COLUMNS"
|
||||
rows <- fromMaybe 24 . (>>= readMaybe) <$> lookupEnv "LINES"
|
||||
cols <- fromMaybe 80 . (>>= readMaybe) <$> lookupEnv "COLUMNS"
|
||||
pure (TerminalSize rows cols)
|
||||
|
||||
queryTerminalSize :: IO (Maybe TerminalSize)
|
||||
@@ -415,7 +476,8 @@ queryTerminalSize = do
|
||||
try $
|
||||
readCreateProcessWithExitCode
|
||||
(proc "sh" ["-c", "stty size </dev/tty"])
|
||||
"" :: IO (Either SomeException (ExitCode, String, String))
|
||||
"" ::
|
||||
IO (Either SomeException (ExitCode, String, String))
|
||||
pure $ do
|
||||
(exitCode, stdoutText, _) <- either (const Nothing) Just result
|
||||
case exitCode of
|
||||
@@ -430,6 +492,46 @@ queryTerminalSize = do
|
||||
watchTimestamp :: IO String
|
||||
watchTimestamp = formatTime defaultTimeLocale "%H:%M:%S" <$> getZonedTime
|
||||
|
||||
watchLoopDelayMicros :: Int
|
||||
watchLoopDelayMicros = 1000000
|
||||
|
||||
watchInputPollMicros :: Int
|
||||
watchInputPollMicros = 100000
|
||||
|
||||
waitForTuiAction :: Bool -> Int -> IO (Maybe TuiAction)
|
||||
waitForTuiAction watchInputEnabled remainingMicros
|
||||
| remainingMicros <= 0 = pure Nothing
|
||||
| otherwise = do
|
||||
nextAction <- pollTuiAction watchInputEnabled
|
||||
case nextAction of
|
||||
Just action -> pure (Just action)
|
||||
Nothing -> do
|
||||
threadDelay (min watchInputPollMicros remainingMicros)
|
||||
waitForTuiAction watchInputEnabled (remainingMicros - watchInputPollMicros)
|
||||
|
||||
pollTuiAction :: Bool -> IO (Maybe TuiAction)
|
||||
pollTuiAction watchInputEnabled
|
||||
| not watchInputEnabled = pure Nothing
|
||||
| otherwise = drainInput
|
||||
where
|
||||
drainInput = do
|
||||
hasInput <- hWaitForInput stdin 0
|
||||
if hasInput
|
||||
then do
|
||||
inputChar <- hGetChar stdin
|
||||
case parseTuiAction inputChar of
|
||||
Just action -> pure (Just action)
|
||||
Nothing -> drainInput
|
||||
else pure Nothing
|
||||
|
||||
parseTuiAction :: Char -> Maybe TuiAction
|
||||
parseTuiAction inputChar = case toLower inputChar of
|
||||
'c' -> Just TuiClean
|
||||
'q' -> Just TuiQuit
|
||||
's' -> Just TuiStopWatching
|
||||
'w' -> Just TuiStartWatching
|
||||
_ -> Nothing
|
||||
|
||||
trimTrailingSpace :: String -> String
|
||||
trimTrailingSpace = reverse . dropWhile isSpace . reverse
|
||||
|
||||
@@ -467,47 +569,107 @@ cleanOptions = Options {verbosity = False, optCommand = Clean}
|
||||
rebuildOptions :: Options
|
||||
rebuildOptions = Options {verbosity = False, optCommand = Rebuild}
|
||||
|
||||
watchOptions :: WatchSettings -> Options
|
||||
watchOptions watchSettings =
|
||||
Options
|
||||
{ verbosity = False,
|
||||
optCommand =
|
||||
Watch
|
||||
{ host = watchHost watchSettings,
|
||||
port = watchPort watchSettings,
|
||||
no_server = not (watchServerEnabled watchSettings)
|
||||
}
|
||||
}
|
||||
|
||||
runSiteCommand :: Configuration -> Options -> FilePath -> IO ExitCode
|
||||
runSiteCommand config options cslPath =
|
||||
hakyllWithExitCodeAndArgs config options (siteRules cslPath)
|
||||
|
||||
runWatch :: FilePath -> Configuration -> FilePath -> WatchSettings -> IO ExitCode
|
||||
runWatch projectRoot config _cslPath watchSettings =
|
||||
runTui :: FilePath -> Configuration -> FilePath -> WatchSettings -> IO ExitCode
|
||||
runTui projectRoot config _cslPath watchSettings = do
|
||||
stdoutInteractive <- hIsTerminalDevice stdout
|
||||
stdinInteractive <- hIsTerminalDevice stdin
|
||||
let watchInputEnabled = stdoutInteractive && stdinInteractive
|
||||
if watchInputEnabled
|
||||
then
|
||||
withWatchTui do
|
||||
serverStatusRef <- newIORef initialServerStatus
|
||||
renderStateRef <- newIORef Nothing
|
||||
startPreviewServer config watchSettings serverStatusRef
|
||||
renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatusRef initialDashboardState
|
||||
(_, initialDashboard) <-
|
||||
runWatchBuild
|
||||
"build"
|
||||
"initial build"
|
||||
"initial build"
|
||||
projectRoot
|
||||
config
|
||||
watchSettings
|
||||
renderStateRef
|
||||
serverStatusRef
|
||||
initialDashboardState
|
||||
initialSnapshot <- snapshotInputs projectRoot
|
||||
watchLoop renderStateRef serverStatusRef initialSnapshot initialDashboard
|
||||
renderWatchDashboard renderStateRef projectRoot watchSettings serverStatusRef initialDashboardState
|
||||
tuiLoop watchInputEnabled renderStateRef serverStatusRef TuiWatchStopped initialDashboardState
|
||||
else do
|
||||
putStrLn "hakysidian -tui requires an interactive terminal."
|
||||
pure (ExitFailure 1)
|
||||
where
|
||||
initialServerStatus
|
||||
| watchServerEnabled watchSettings = ServerStarting
|
||||
| otherwise = ServerDisabled
|
||||
|
||||
watchLoop ::
|
||||
tuiLoop ::
|
||||
Bool ->
|
||||
IORef (Maybe (TerminalSize, ServerStatus, DashboardState)) ->
|
||||
IORef ServerStatus ->
|
||||
FileSnapshot ->
|
||||
TuiWatchState ->
|
||||
DashboardState ->
|
||||
IO ExitCode
|
||||
watchLoop renderStateRef serverStatusRef previousSnapshot dashboard = do
|
||||
renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatusRef dashboard
|
||||
threadDelay 1000000
|
||||
tuiLoop watchInputEnabled renderStateRef serverStatusRef watchState dashboard = do
|
||||
renderWatchDashboard renderStateRef projectRoot watchSettings serverStatusRef dashboard
|
||||
nextAction <- waitForTuiAction watchInputEnabled watchLoopDelayMicros
|
||||
case nextAction of
|
||||
Just TuiQuit -> pure ExitSuccess
|
||||
Just TuiStartWatching -> case watchState of
|
||||
TuiWatching _ ->
|
||||
tuiLoop watchInputEnabled renderStateRef serverStatusRef watchState dashboard
|
||||
TuiWatchStopped -> do
|
||||
(_, nextDashboard) <-
|
||||
runDashboardCommand
|
||||
"rebuild"
|
||||
"watch start"
|
||||
"manual start"
|
||||
"building (watch start)"
|
||||
(watchCommandStatus "watch start")
|
||||
projectRoot
|
||||
watchSettings
|
||||
renderStateRef
|
||||
serverStatusRef
|
||||
dashboard
|
||||
nextSnapshot <- snapshotInputs projectRoot
|
||||
tuiLoop watchInputEnabled renderStateRef serverStatusRef (TuiWatching nextSnapshot) nextDashboard
|
||||
Just TuiStopWatching -> case watchState of
|
||||
TuiWatchStopped ->
|
||||
tuiLoop watchInputEnabled renderStateRef serverStatusRef watchState dashboard
|
||||
TuiWatching _ -> do
|
||||
nextDashboard <-
|
||||
appendDashboardMessage
|
||||
( dashboard
|
||||
{ dashboardStatus = "idle",
|
||||
dashboardLastChange = "watch stopped"
|
||||
}
|
||||
)
|
||||
"watch stopped"
|
||||
tuiLoop watchInputEnabled renderStateRef serverStatusRef TuiWatchStopped nextDashboard
|
||||
Just TuiClean -> do
|
||||
(_, nextDashboard) <-
|
||||
runDashboardCommand
|
||||
"clean"
|
||||
"clean"
|
||||
"manual clean"
|
||||
"cleaning"
|
||||
cleanCommandStatus
|
||||
projectRoot
|
||||
watchSettings
|
||||
renderStateRef
|
||||
serverStatusRef
|
||||
dashboard
|
||||
tuiLoop watchInputEnabled renderStateRef serverStatusRef TuiWatchStopped nextDashboard
|
||||
Nothing -> case watchState of
|
||||
TuiWatchStopped ->
|
||||
tuiLoop watchInputEnabled renderStateRef serverStatusRef watchState dashboard
|
||||
TuiWatching previousSnapshot -> do
|
||||
nextSnapshot <- snapshotInputs projectRoot
|
||||
if nextSnapshot == previousSnapshot
|
||||
then watchLoop renderStateRef serverStatusRef previousSnapshot dashboard
|
||||
then tuiLoop watchInputEnabled renderStateRef serverStatusRef watchState dashboard
|
||||
else do
|
||||
let changedFiles = diffSnapshots previousSnapshot nextSnapshot
|
||||
command :: String
|
||||
@@ -517,52 +679,66 @@ runWatch projectRoot config _cslPath watchSettings =
|
||||
else "build"
|
||||
changeSummary = intercalate ", " changedFiles
|
||||
(_, nextDashboard) <-
|
||||
runWatchBuild
|
||||
runDashboardCommand
|
||||
command
|
||||
command
|
||||
changeSummary
|
||||
("building (" ++ command ++ ")")
|
||||
(watchCommandStatus command)
|
||||
projectRoot
|
||||
config
|
||||
watchSettings
|
||||
renderStateRef
|
||||
serverStatusRef
|
||||
dashboard
|
||||
watchLoop renderStateRef serverStatusRef nextSnapshot nextDashboard
|
||||
tuiLoop watchInputEnabled renderStateRef serverStatusRef (TuiWatching nextSnapshot) nextDashboard
|
||||
|
||||
watchCommandStatus :: String -> ExitCode -> String
|
||||
watchCommandStatus label exitCode
|
||||
| exitCode == ExitSuccess = "watching"
|
||||
| otherwise = "watching after failed " ++ label
|
||||
|
||||
cleanCommandStatus :: ExitCode -> String
|
||||
cleanCommandStatus exitCode
|
||||
| exitCode == ExitSuccess = "idle"
|
||||
| otherwise = "idle after failed clean"
|
||||
|
||||
renderBuildResult :: ExitCode -> String
|
||||
renderBuildResult ExitSuccess = "success"
|
||||
renderBuildResult (ExitFailure code) = "failed (" ++ show code ++ ")"
|
||||
|
||||
runWatchBuild ::
|
||||
appendDashboardMessage :: DashboardState -> String -> IO DashboardState
|
||||
appendDashboardMessage dashboard message = do
|
||||
timestamp <- watchTimestamp
|
||||
pure (appendLogBatch dashboard message timestamp [])
|
||||
|
||||
runDashboardCommand ::
|
||||
String ->
|
||||
String ->
|
||||
String ->
|
||||
String ->
|
||||
(ExitCode -> String) ->
|
||||
FilePath ->
|
||||
Configuration ->
|
||||
WatchSettings ->
|
||||
IORef (Maybe (TerminalSize, ServerStatus, DashboardState)) ->
|
||||
IORef ServerStatus ->
|
||||
DashboardState ->
|
||||
IO (ExitCode, DashboardState)
|
||||
runWatchBuild command label changeSummary projectRoot config watchSettings renderStateRef serverStatusRef dashboard = do
|
||||
runDashboardCommand command label changeSummary runningStatus completedStatus projectRoot watchSettings renderStateRef serverStatusRef dashboard = do
|
||||
startedAt <- watchTimestamp
|
||||
let runningDashboard =
|
||||
dashboard
|
||||
{ dashboardStatus = "building (" ++ label ++ ")",
|
||||
{ dashboardStatus = runningStatus,
|
||||
dashboardLastChange = changeSummary,
|
||||
dashboardLastBuild = "running since " ++ startedAt
|
||||
}
|
||||
renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatusRef runningDashboard
|
||||
renderWatchDashboard renderStateRef projectRoot watchSettings serverStatusRef runningDashboard
|
||||
(exitCode, buildLines) <- runCapturedSiteCommand projectRoot command
|
||||
finishedAt <- watchTimestamp
|
||||
let loggedDashboard =
|
||||
appendLogBatch runningDashboard (label ++ ": " ++ changeSummary) finishedAt buildLines
|
||||
completedDashboard =
|
||||
loggedDashboard
|
||||
{ dashboardStatus =
|
||||
if exitCode == ExitSuccess
|
||||
then "watching"
|
||||
else "watching after failed " ++ label,
|
||||
{ dashboardStatus = completedStatus exitCode,
|
||||
dashboardLastBuild =
|
||||
renderBuildResult exitCode
|
||||
++ " at "
|
||||
@@ -570,7 +746,7 @@ runWatchBuild command label changeSummary projectRoot config watchSettings rende
|
||||
++ " via "
|
||||
++ label
|
||||
}
|
||||
renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatusRef completedDashboard
|
||||
renderWatchDashboard renderStateRef projectRoot watchSettings serverStatusRef completedDashboard
|
||||
pure (exitCode, completedDashboard)
|
||||
|
||||
startPreviewServer :: Configuration -> WatchSettings -> IORef ServerStatus -> IO ()
|
||||
@@ -580,10 +756,11 @@ startPreviewServer config watchSettings serverStatusRef
|
||||
forkIO $
|
||||
do
|
||||
result <-
|
||||
(try $
|
||||
( try $
|
||||
Warp.runSettings settings $
|
||||
staticApp $
|
||||
previewSettings config (destinationDirectory config)) ::
|
||||
previewSettings config (destinationDirectory config)
|
||||
) ::
|
||||
IO (Either SomeException ())
|
||||
case result of
|
||||
Left err -> writeIORef serverStatusRef (ServerFailed (show err))
|
||||
@@ -593,7 +770,8 @@ startPreviewServer config watchSettings serverStatusRef
|
||||
settings =
|
||||
Warp.setBeforeMainLoop (writeIORef serverStatusRef ServerRunning) $
|
||||
Warp.setPort (watchPort watchSettings) $
|
||||
Warp.setHost (fromString (watchHost watchSettings)) $
|
||||
Warp.setHost
|
||||
(fromString (watchHost watchSettings))
|
||||
Warp.defaultSettings
|
||||
|
||||
snapshotInputs :: FilePath -> IO FileSnapshot
|
||||
@@ -625,8 +803,8 @@ trackedFilesIn root = do
|
||||
if exists
|
||||
then do
|
||||
entries <- listDirectory root
|
||||
fmap concat $
|
||||
traverse
|
||||
fmap concat
|
||||
<$> traverse
|
||||
( \name -> do
|
||||
let path = root </> name
|
||||
isDir <- doesDirectoryExist path
|
||||
|
||||
Reference in New Issue
Block a user