mirror of
http://101.35.51.105:3000/congyu/Hakysidian.git
synced 2026-04-28 05:50:49 +08:00
Compare commits
2 Commits
3652459503
...
3a3fd97055
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a3fd97055 | |||
| aa05d73c9c |
+51
-38
@@ -1,90 +1,103 @@
|
||||
.theorem-environment {
|
||||
font-style: italic;
|
||||
margin-top: 1em;
|
||||
padding: 0.5em;
|
||||
background-color: whitesmoke;
|
||||
font-style: italic;
|
||||
margin-top: 1em;
|
||||
padding: 0.5em;
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
|
||||
.theorem-header {
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: inline;
|
||||
.theorem-header + p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.Proof .type {
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.Proof {
|
||||
background: none;
|
||||
font-style: normal;
|
||||
position: relative;
|
||||
background: none;
|
||||
font-style: normal;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Proof:after {
|
||||
content: '∎';
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
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%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.postindex cite {
|
||||
font-style: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
table.postindex td.right {
|
||||
text-align: right;
|
||||
width: 11ex;
|
||||
text-align: right;
|
||||
width: 11ex;
|
||||
}
|
||||
|
||||
.header-section-number {
|
||||
margin-right: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.header-section-number:after {
|
||||
content: '.';
|
||||
content: ".";
|
||||
}
|
||||
|
||||
.csl-entry {
|
||||
display: table;
|
||||
width: 100%;
|
||||
table-layout: auto;
|
||||
display: table;
|
||||
width: 100%;
|
||||
table-layout: auto;
|
||||
}
|
||||
|
||||
.csl-left-margin {
|
||||
display: table-cell;
|
||||
padding-right: 0.5em;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
display: table-cell;
|
||||
padding-right: 0.5em;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.csl-right-inline {
|
||||
display: table-cell;
|
||||
display: table-cell;
|
||||
}
|
||||
.csl-right-inline a {
|
||||
word-break: break-all;
|
||||
}
|
||||
.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;
|
||||
|
||||
+33
-34
@@ -1,52 +1,51 @@
|
||||
|
||||
/* fonts */
|
||||
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src: url("/fonts/Lato-Regular.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-family: "Lato";
|
||||
src: url("/fonts/Lato-Regular.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src: url("/fonts/Lato-Bold.woff2") format("woff2");
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-family: "Lato";
|
||||
src: url("/fonts/Lato-Bold.woff2") format("woff2");
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src: url("/fonts/Lato-Italic.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
font-family: "Lato";
|
||||
src: url("/fonts/Lato-Italic.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src: url("/fonts/Lato-BoldItalic.woff2") format("woff2");
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
font-family: "Lato";
|
||||
src: url("/fonts/Lato-BoldItalic.woff2") format("woff2");
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lete Sans Math";
|
||||
src: url("/fonts/LeteSansMath.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-family: "Lete Sans Math";
|
||||
src: url("/fonts/LeteSansMath.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Lete Sans Math";
|
||||
src: url("/fonts/LeteSansMath-Bold.woff2") format("woff2");
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-family: "Lete Sans Math";
|
||||
src: url("/fonts/LeteSansMath-Bold.woff2") format("woff2");
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IosevkaC";
|
||||
src: url("/fonts/IosevkaCustom-Regular.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-family: "IosevkaC";
|
||||
src: url("/fonts/IosevkaCustom-Regular.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IosevkaC";
|
||||
src: url("/fonts/IosevkaCustom-Bold.woff2") format("woff2");
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
font-family: "IosevkaC";
|
||||
src: url("/fonts/IosevkaCustom-Bold.woff2") format("woff2");
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ cabal install exe:hakysidian
|
||||
|
||||
## Commands
|
||||
|
||||
The CLI mirrors the common Hakyll workflow:
|
||||
The default CLI mirrors the common Hakyll workflow:
|
||||
|
||||
```bash
|
||||
hakysidian build
|
||||
@@ -70,28 +70,41 @@ 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`: shows an in-place terminal dashboard, watches project inputs, and rebuilds automatically on change.
|
||||
- `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 Mode
|
||||
## Watch And TUI
|
||||
|
||||
`watch` tracks:
|
||||
Both `watch` and `-tui` work against the same project inputs:
|
||||
|
||||
- `notes/**`
|
||||
- `reference.bib`
|
||||
- `math-macros.md`
|
||||
- `images/**`
|
||||
|
||||
The watch UI:
|
||||
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,
|
||||
- avoids scrolling raw Hakyll logs through the terminal,
|
||||
- can start a local preview server unless `--no-server` is passed.
|
||||
- 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
|
||||
|
||||
|
||||
+182
-92
@@ -9,7 +9,7 @@ import ChaoDoc
|
||||
import Control.Concurrent (forkIO, threadDelay)
|
||||
import Control.Exception (SomeException, bracket_, try)
|
||||
import Control.Monad (filterM, unless, void, when)
|
||||
import Data.Char (isSpace)
|
||||
import Data.Char (isSpace, toLower)
|
||||
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
|
||||
import Data.Kind (Type)
|
||||
import Data.List (intercalate, isPrefixOf, sort, sortOn)
|
||||
@@ -112,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
|
||||
@@ -203,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/."
|
||||
]
|
||||
@@ -223,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)
|
||||
|
||||
@@ -251,12 +269,12 @@ validateProject projectRoot = do
|
||||
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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -353,12 +371,11 @@ watchInputMode inputMode =
|
||||
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
|
||||
@@ -369,22 +386,20 @@ 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 ("Change : " ++ dashboardLastChange 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,
|
||||
dashboardRow cols "Controls: q quit, Ctrl-C interrupt",
|
||||
dashboardRow cols "Controls: w watch, s stop, c clean, q quit, Ctrl-C interrupt",
|
||||
border
|
||||
]
|
||||
availableLogRows = max 1 (rows - length headerRows - length footerRows)
|
||||
@@ -483,20 +498,20 @@ watchLoopDelayMicros = 1000000
|
||||
watchInputPollMicros :: Int
|
||||
watchInputPollMicros = 100000
|
||||
|
||||
waitForWatchQuit :: Bool -> Int -> IO Bool
|
||||
waitForWatchQuit watchInputEnabled remainingMicros
|
||||
| remainingMicros <= 0 = pure False
|
||||
waitForTuiAction :: Bool -> Int -> IO (Maybe TuiAction)
|
||||
waitForTuiAction watchInputEnabled remainingMicros
|
||||
| remainingMicros <= 0 = pure Nothing
|
||||
| otherwise = do
|
||||
shouldQuit <- pollWatchQuit watchInputEnabled
|
||||
if shouldQuit
|
||||
then pure True
|
||||
else do
|
||||
nextAction <- pollTuiAction watchInputEnabled
|
||||
case nextAction of
|
||||
Just action -> pure (Just action)
|
||||
Nothing -> do
|
||||
threadDelay (min watchInputPollMicros remainingMicros)
|
||||
waitForWatchQuit watchInputEnabled (remainingMicros - watchInputPollMicros)
|
||||
waitForTuiAction watchInputEnabled (remainingMicros - watchInputPollMicros)
|
||||
|
||||
pollWatchQuit :: Bool -> IO Bool
|
||||
pollWatchQuit watchInputEnabled
|
||||
| not watchInputEnabled = pure False
|
||||
pollTuiAction :: Bool -> IO (Maybe TuiAction)
|
||||
pollTuiAction watchInputEnabled
|
||||
| not watchInputEnabled = pure Nothing
|
||||
| otherwise = drainInput
|
||||
where
|
||||
drainInput = do
|
||||
@@ -504,10 +519,18 @@ pollWatchQuit watchInputEnabled
|
||||
if hasInput
|
||||
then do
|
||||
inputChar <- hGetChar stdin
|
||||
if inputChar == 'q'
|
||||
then pure True
|
||||
else drainInput
|
||||
else pure False
|
||||
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
|
||||
@@ -546,109 +569,176 @@ 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 = do
|
||||
runTui :: FilePath -> Configuration -> FilePath -> WatchSettings -> IO ExitCode
|
||||
runTui projectRoot config _cslPath watchSettings = do
|
||||
stdoutInteractive <- hIsTerminalDevice stdout
|
||||
stdinInteractive <- hIsTerminalDevice stdin
|
||||
let watchInputEnabled = stdoutInteractive && stdinInteractive
|
||||
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 watchInputEnabled renderStateRef serverStatusRef initialSnapshot initialDashboard
|
||||
if watchInputEnabled
|
||||
then
|
||||
withWatchTui do
|
||||
serverStatusRef <- newIORef initialServerStatus
|
||||
renderStateRef <- newIORef Nothing
|
||||
startPreviewServer config watchSettings serverStatusRef
|
||||
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 watchInputEnabled renderStateRef serverStatusRef previousSnapshot dashboard = do
|
||||
renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatusRef dashboard
|
||||
shouldQuit <- waitForWatchQuit watchInputEnabled watchLoopDelayMicros
|
||||
if shouldQuit
|
||||
then pure ExitSuccess
|
||||
else do
|
||||
nextSnapshot <- snapshotInputs projectRoot
|
||||
if nextSnapshot == previousSnapshot
|
||||
then watchLoop watchInputEnabled renderStateRef serverStatusRef previousSnapshot dashboard
|
||||
else do
|
||||
let changedFiles = diffSnapshots previousSnapshot nextSnapshot
|
||||
command :: String
|
||||
command =
|
||||
if any (`M.notMember` nextSnapshot) (M.keys previousSnapshot)
|
||||
then "rebuild"
|
||||
else "build"
|
||||
changeSummary = intercalate ", " changedFiles
|
||||
(_, nextDashboard) <-
|
||||
runWatchBuild
|
||||
command
|
||||
command
|
||||
changeSummary
|
||||
projectRoot
|
||||
config
|
||||
watchSettings
|
||||
renderStateRef
|
||||
serverStatusRef
|
||||
dashboard
|
||||
watchLoop watchInputEnabled renderStateRef serverStatusRef nextSnapshot nextDashboard
|
||||
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 tuiLoop watchInputEnabled renderStateRef serverStatusRef watchState dashboard
|
||||
else do
|
||||
let changedFiles = diffSnapshots previousSnapshot nextSnapshot
|
||||
command :: String
|
||||
command =
|
||||
if any (`M.notMember` nextSnapshot) (M.keys previousSnapshot)
|
||||
then "rebuild"
|
||||
else "build"
|
||||
changeSummary = intercalate ", " changedFiles
|
||||
(_, nextDashboard) <-
|
||||
runDashboardCommand
|
||||
command
|
||||
command
|
||||
changeSummary
|
||||
("building (" ++ command ++ ")")
|
||||
(watchCommandStatus command)
|
||||
projectRoot
|
||||
watchSettings
|
||||
renderStateRef
|
||||
serverStatusRef
|
||||
dashboard
|
||||
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 "
|
||||
@@ -656,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 ()
|
||||
|
||||
Reference in New Issue
Block a user