├── .github
├── CODEOWNERS
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yml
└── workflows
│ ├── actionlint.yml
│ ├── dprint.yml
│ ├── firebase-hosting-merge.yml
│ ├── firebase-hosting-pull-request.yml
│ ├── hashira-cui--release.yml
│ ├── hashira-cui--test.yml
│ ├── hashira-web--test.yml
│ ├── hashira-web-functions--test.yml
│ ├── merge-bot-pr.yml
│ └── pages.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yaml
├── LICENSE
├── Makefile
├── README.md
├── client
└── client.go
├── cmd
├── hashira-cui
│ ├── ctrl.go
│ ├── cursor.go
│ ├── delegater.go
│ ├── editor.go
│ ├── keybinding.go
│ ├── keyedtask.go
│ ├── main.go
│ ├── model.go
│ ├── pane.go
│ ├── pubsub.go
│ ├── view.go
│ └── view_test.go
├── hashira-web-client
│ └── main.go
├── hashira
│ ├── commands.go
│ └── main.go
└── hashirad
│ └── main.go
├── daemon
├── daemon.go
├── grpc.go
└── grpc_test.go
├── database
├── boltdb.go
├── boltdb_test.go
└── database.go
├── docs
├── Makefile
├── book.toml
├── book
│ ├── .nojekyll
│ ├── 404.html
│ ├── FontAwesome
│ │ ├── css
│ │ │ └── font-awesome.css
│ │ └── fonts
│ │ │ ├── FontAwesome.ttf
│ │ │ ├── fontawesome-webfont.eot
│ │ │ ├── fontawesome-webfont.svg
│ │ │ ├── fontawesome-webfont.ttf
│ │ │ ├── fontawesome-webfont.woff
│ │ │ └── fontawesome-webfont.woff2
│ ├── assets
│ │ └── images
│ │ │ └── uml
│ │ │ └── 636bfd90b359509ea12ac7f7ca3fc622a6af8f45.svg
│ ├── ayu-highlight.css
│ ├── book.js
│ ├── clipboard.min.js
│ ├── communication
│ │ ├── daemon2datastore.html
│ │ ├── front2daemon.html
│ │ └── overview.html
│ ├── css
│ │ ├── chrome.css
│ │ ├── general.css
│ │ ├── print.css
│ │ └── variables.css
│ ├── daemon
│ │ ├── entity.html
│ │ └── overview.html
│ ├── elasticlunr.min.js
│ ├── favicon.png
│ ├── favicon.svg
│ ├── fonts
│ │ ├── OPEN-SANS-LICENSE.txt
│ │ ├── SOURCE-CODE-PRO-LICENSE.txt
│ │ ├── fonts.css
│ │ ├── open-sans-v17-all-charsets-300.woff2
│ │ ├── open-sans-v17-all-charsets-300italic.woff2
│ │ ├── open-sans-v17-all-charsets-600.woff2
│ │ ├── open-sans-v17-all-charsets-600italic.woff2
│ │ ├── open-sans-v17-all-charsets-700.woff2
│ │ ├── open-sans-v17-all-charsets-700italic.woff2
│ │ ├── open-sans-v17-all-charsets-800.woff2
│ │ ├── open-sans-v17-all-charsets-800italic.woff2
│ │ ├── open-sans-v17-all-charsets-italic.woff2
│ │ ├── open-sans-v17-all-charsets-regular.woff2
│ │ └── source-code-pro-v11-all-charsets-500.woff2
│ ├── highlight.css
│ ├── highlight.js
│ ├── index.html
│ ├── mark.min.js
│ ├── mdbook-plantuml-img
│ │ ├── 318b7e10f89ecde5b196f1d82d952bd7629d460b.svg
│ │ ├── 35fa4c1648d5063cde4624ce5a91d67b790c002f.svg
│ │ ├── b356288d94cf36c2c956a9219d6b5a174a0679ba.svg
│ │ ├── b9d8843abdb2735cbc2ef31cdf6cc5be7035e15f.svg
│ │ └── f3586f2bf96d39cc9734911942cc03d4b4137c1e.svg
│ ├── overview.html
│ ├── print.html
│ ├── prototype.html
│ ├── searcher.js
│ ├── searchindex.js
│ ├── searchindex.json
│ ├── tables.html
│ └── tomorrow-night.css
└── src
│ ├── README.md
│ ├── SUMMARY.md
│ ├── assets
│ └── images
│ │ └── uml
│ │ └── 636bfd90b359509ea12ac7f7ca3fc622a6af8f45.svg
│ ├── communication
│ ├── daemon2datastore.md
│ ├── front2daemon.md
│ └── overview.md
│ ├── daemon
│ ├── entity.md
│ └── overview.md
│ ├── mdbook-plantuml-img
│ ├── 318b7e10f89ecde5b196f1d82d952bd7629d460b.svg
│ ├── 35fa4c1648d5063cde4624ce5a91d67b790c002f.svg
│ ├── b356288d94cf36c2c956a9219d6b5a174a0679ba.svg
│ ├── b9d8843abdb2735cbc2ef31cdf6cc5be7035e15f.svg
│ └── f3586f2bf96d39cc9734911942cc03d4b4137c1e.svg
│ ├── overview.md
│ ├── prototype.md
│ └── tables.md
├── dprint.json
├── go.mod
├── go.sum
├── hashira-web
├── .firebase
│ └── hosting.cHVibGlj.cache
├── .firebaserc
├── .gitignore
├── .tool-versions
├── assets
│ ├── android-icon-144x144.png
│ ├── android-icon-192x192.png
│ ├── android-icon-36x36.png
│ ├── android-icon-48x48.png
│ ├── android-icon-72x72.png
│ ├── android-icon-96x96.png
│ ├── apple-icon-114x114.png
│ ├── apple-icon-120x120.png
│ ├── apple-icon-144x144.png
│ ├── apple-icon-152x152.png
│ ├── apple-icon-180x180.png
│ ├── apple-icon-57x57.png
│ ├── apple-icon-60x60.png
│ ├── apple-icon-72x72.png
│ ├── apple-icon-76x76.png
│ ├── apple-icon-precomposed.png
│ ├── apple-icon.png
│ ├── browserconfig.xml
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon-96x96.png
│ ├── favicon.ico
│ ├── manifest.json
│ ├── ms-icon-144x144.png
│ ├── ms-icon-150x150.png
│ ├── ms-icon-310x310.png
│ ├── ms-icon-70x70.png
│ └── service-worker.js
├── firebase.json
├── firestore.indexes.json
├── firestore.rules
├── functions
│ ├── .gcloudignore
│ ├── .gitignore
│ ├── Makefile
│ ├── func.go
│ ├── go.mod
│ ├── go.sum
│ └── hashira
│ │ ├── accesstoken.go
│ │ ├── accesstoken_test.go
│ │ ├── add.go
│ │ ├── download.go
│ │ ├── download_test.go
│ │ ├── entities.go
│ │ ├── hashira.go
│ │ ├── hashira_test.go
│ │ ├── merge.go
│ │ ├── merge_test.go
│ │ ├── store.go
│ │ ├── store
│ │ ├── atstore.go
│ │ └── tpstore.go
│ │ ├── upload.go
│ │ └── upload_test.go
├── index.html
├── package.json
├── src
│ ├── AccessToken.tsx
│ ├── App.tsx
│ ├── Header.tsx
│ ├── Router.tsx
│ ├── Tags.tsx
│ ├── TaskInput.tsx
│ ├── TaskList.tsx
│ ├── env.d.ts
│ ├── firebase.ts
│ ├── hooks.ts
│ ├── index.tsx
│ ├── revision.ts
│ ├── styles.ts
│ ├── task.ts
│ └── types.ts
├── tsconfig.json
├── vite.config.ts
└── yarn.lock
├── proto
└── hashira.proto
├── service
├── hashira.pb.go
└── hashira_grpc.pb.go
├── sync
├── client.go
├── package.go
└── syncutil
│ ├── client.go
│ ├── download.go
│ ├── testaccesstoken.go
│ └── upload.go
├── tool
├── Makefile
├── protoc_gen_go_install.sh
└── protoc_install.sh
└── xdg
├── xdg.go
└── xdg_test.go
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @pankona
2 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Pull request for issue #{issue number}
2 |
3 | ## Updates
4 |
5 | -
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: gomod
4 | directory: "/"
5 | schedule:
6 | interval: monthly
7 | open-pull-requests-limit: 10
8 | - package-ecosystem: npm
9 | directory: "/hashira-web"
10 | schedule:
11 | interval: monthly
12 | open-pull-requests-limit: 10
13 | groups:
14 | react-dependencies: # See #1352 for details
15 | patterns:
16 | - "*react*"
17 | ignore:
18 | - dependency-name: "@types/node"
19 | # https://github.com/vitejs/vite/issues/15714#issuecomment-1949236530
20 | # https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68300
21 | update-types:
22 | ["version-update:semver-major", "version-update:semver-minor"]
23 | - dependency-name: "styled-components"
24 | # https://github.com/pankona/hashira/issues/1162#issuecomment-1950253823
25 | update-types: ["version-update:semver-major"]
26 | - dependency-name: "vite"
27 | # https://github.com/pankona/hashira/issues/1162#issuecomment-1950257529
28 | update-types: ["version-update:semver-major"]
29 | - package-ecosystem: gomod
30 | directory: "/hashira-web/functions"
31 | schedule:
32 | interval: monthly
33 | open-pull-requests-limit: 10
34 | - package-ecosystem: "github-actions"
35 | directory: "/"
36 | schedule:
37 | interval: "monthly"
38 |
--------------------------------------------------------------------------------
/.github/workflows/actionlint.yml:
--------------------------------------------------------------------------------
1 | name: Lint GitHub Actions workflows
2 | on:
3 | push:
4 | branches:
5 | - master
6 | paths:
7 | - ".github/workflows/**"
8 | pull_request:
9 | paths:
10 | - ".github/workflows/**"
11 |
12 | jobs:
13 | actionlint:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | - name: Download actionlint
18 | id: get_actionlint
19 | run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
20 | shell: bash
21 | - name: Check workflow files
22 | run: ${{ steps.get_actionlint.outputs.executable }} -color
23 | shell: bash
24 |
--------------------------------------------------------------------------------
/.github/workflows/dprint.yml:
--------------------------------------------------------------------------------
1 | name: dprint
2 | on:
3 | push:
4 | branches: [master]
5 | pull_request:
6 |
7 | jobs:
8 | check:
9 | timeout-minutes: 15
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: dprint/check@v2.2
14 | with:
15 | dprint-version: "0.48.0"
16 |
--------------------------------------------------------------------------------
/.github/workflows/firebase-hosting-merge.yml:
--------------------------------------------------------------------------------
1 | # This file was auto-generated by the Firebase CLI
2 | # https://github.com/firebase/firebase-tools
3 |
4 | name: Deploy to Firebase Hosting on merge
5 |
6 | on:
7 | push:
8 | branches:
9 | - master
10 | workflow_dispatch:
11 |
12 | defaults:
13 | run:
14 | working-directory: hashira-web
15 |
16 | jobs:
17 | build_and_deploy:
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/checkout@v4
21 | - uses: actions/setup-node@v4
22 | with:
23 | node-version-file: "hashira-web/.tool-versions"
24 | cache: "yarn"
25 | cache-dependency-path: "hashira-web/yarn.lock"
26 | - run: yarn install --frozen-lockfile
27 | - run: VITE_REVISION="Rev ${{ github.sha }} Build at $(date)" yarn build
28 | - uses: FirebaseExtended/action-hosting-deploy@v0
29 | with:
30 | entryPoint: hashira-web
31 | repoToken: "${{ secrets.GITHUB_TOKEN }}"
32 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_HASHIRA_WEB }}"
33 | channelId: live
34 | projectId: hashira-web
35 |
--------------------------------------------------------------------------------
/.github/workflows/firebase-hosting-pull-request.yml:
--------------------------------------------------------------------------------
1 | # This file was auto-generated by the Firebase CLI
2 | # https://github.com/firebase/firebase-tools
3 |
4 | name: Deploy to Firebase Hosting on PR
5 |
6 | on:
7 | pull_request:
8 | types: [opened, synchronize, reopened, labeled]
9 | paths:
10 | - ".github/workflows/firebase-hosting-pull-request.yml"
11 | - "hashira-web/**"
12 |
13 | defaults:
14 | run:
15 | working-directory: hashira-web
16 |
17 | jobs:
18 | build_and_preview:
19 | if: contains(github.event.pull_request.labels.*.name, 'ok to test') && (github.triggering_actor == 'pankona')
20 | runs-on: ubuntu-latest
21 | steps:
22 | - uses: actions/checkout@v4
23 | - uses: actions/setup-node@v4
24 | with:
25 | node-version-file: "hashira-web/.tool-versions"
26 | cache: "yarn"
27 | cache-dependency-path: "hashira-web/yarn.lock"
28 | - run: yarn install --frozen-lockfile
29 | - run: VITE_REVISION="Rev ${{ github.event.pull_request.head.sha }} Build at $(date)" yarn build
30 | - uses: FirebaseExtended/action-hosting-deploy@v0
31 | with:
32 | entryPoint: hashira-web
33 | repoToken: "${{ secrets.GITHUB_TOKEN }}"
34 | firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_HASHIRA_WEB }}"
35 | projectId: hashira-web
36 |
--------------------------------------------------------------------------------
/.github/workflows/hashira-cui--release.yml:
--------------------------------------------------------------------------------
1 | name: hashira-cui - release
2 | on:
3 | push:
4 | tags:
5 | - "v[0-9]+.[0-9]+.[0-9]+"
6 | jobs:
7 | release:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v4
12 | with:
13 | fetch-depth: 0
14 | - name: Setup Go
15 | uses: actions/setup-go@v5
16 | with:
17 | go-version-file: "go.mod"
18 | cache-dependency-path: "go.sum"
19 | - name: Run GoReleaser
20 | uses: goreleaser/goreleaser-action@v6
21 | with:
22 | distribution: goreleaser
23 | version: latest
24 | args: release --clean
25 | env:
26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27 |
--------------------------------------------------------------------------------
/.github/workflows/hashira-cui--test.yml:
--------------------------------------------------------------------------------
1 | name: hashira-cui - CI
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | paths:
7 | - "**"
8 | - "!hashira-web/**"
9 | - "!.github/workflows/**"
10 | - ".github/workflows/hashira-cui--test.yml"
11 | pull_request:
12 | paths:
13 | - "**"
14 | - "!hashira-web/**"
15 | - "!.github/workflows/**"
16 | - ".github/workflows/hashira-cui--test.yml"
17 | jobs:
18 | build:
19 | runs-on: ubuntu-latest
20 | steps:
21 | - uses: actions/checkout@v4
22 | - uses: actions/setup-go@v5
23 | with:
24 | go-version-file: "go.mod"
25 | cache-dependency-path: "go.sum"
26 | - run: make
27 | test:
28 | runs-on: ubuntu-latest
29 | steps:
30 | - uses: actions/checkout@v4
31 | - uses: actions/setup-go@v5
32 | with:
33 | go-version-file: "go.mod"
34 | cache-dependency-path: "go.sum"
35 | - run: make test
36 | lint:
37 | runs-on: ubuntu-latest
38 | steps:
39 | - uses: actions/checkout@v4
40 | - uses: actions/setup-go@v5
41 | with:
42 | go-version-file: "go.mod"
43 | cache-dependency-path: "go.sum"
44 | - uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0
45 | with:
46 | version: v2.0.2
47 |
--------------------------------------------------------------------------------
/.github/workflows/hashira-web--test.yml:
--------------------------------------------------------------------------------
1 | name: hashira-web
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | paths:
7 | - ".github/workflows/hashira-web--test.yml"
8 | - "hashira-web/**"
9 | - "!hashira-web/functions/**"
10 | pull_request:
11 | paths:
12 | - ".github/workflows/hashira-web--test.yml"
13 | - "hashira-web/**"
14 | - "!hashira-web/functions/**"
15 |
16 | defaults:
17 | run:
18 | working-directory: ./hashira-web
19 |
20 | jobs:
21 | build:
22 | runs-on: ubuntu-latest
23 | steps:
24 | - uses: actions/checkout@v4
25 | - uses: actions/setup-node@v4
26 | with:
27 | node-version-file: "hashira-web/.tool-versions"
28 | cache: "yarn"
29 | cache-dependency-path: "hashira-web/yarn.lock"
30 | - run: yarn install --frozen-lockfile
31 | - run: yarn build
32 | typecheck:
33 | runs-on: ubuntu-latest
34 | steps:
35 | - uses: actions/checkout@v4
36 | - uses: actions/setup-node@v4
37 | with:
38 | node-version-file: "hashira-web/.tool-versions"
39 | cache: "yarn"
40 | cache-dependency-path: "hashira-web/yarn.lock"
41 | - run: yarn install --frozen-lockfile
42 | - run: yarn typecheck
43 |
--------------------------------------------------------------------------------
/.github/workflows/hashira-web-functions--test.yml:
--------------------------------------------------------------------------------
1 | name: hashira-web-functions
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | paths:
7 | - ".github/workflows/hashira-web-functions--test.yml"
8 | - "hashira-web/functions/**"
9 | pull_request:
10 | paths:
11 | - ".github/workflows/hashira-web-functions--test.yml"
12 | - "hashira-web/functions/**"
13 |
14 | defaults:
15 | run:
16 | working-directory: ./hashira-web/functions
17 |
18 | jobs:
19 | functions-test:
20 | runs-on: ubuntu-latest
21 | steps:
22 | - uses: actions/checkout@v4
23 | - uses: actions/setup-go@v5
24 | with:
25 | go-version-file: "hashira-web/functions/go.mod"
26 | cache-dependency-path: "hashira-web/functions/go.sum"
27 | - run: go test ./...
28 |
--------------------------------------------------------------------------------
/.github/workflows/merge-bot-pr.yml:
--------------------------------------------------------------------------------
1 | name: Merge bot PR after CI
2 | on: pull_request
3 |
4 | permissions:
5 | contents: write
6 | pull-requests: write
7 |
8 | jobs:
9 | dependabot:
10 | timeout-minutes: 30
11 | runs-on: ubuntu-latest
12 | if: ${{ github.actor == 'dependabot[bot]' }}
13 | steps:
14 | - name: Dependabot metadata
15 | id: metadata
16 | uses: dependabot/fetch-metadata@v2
17 | - name: Wait other jobs
18 | # if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}}
19 | uses: kachick/wait-other-jobs@v3
20 | timeout-minutes: 15
21 | - name: Approve and merge
22 | # if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}}
23 | run: gh pr review --approve "$PR_URL" && gh pr merge --auto --squash "$PR_URL"
24 | env:
25 | PR_URL: ${{github.event.pull_request.html_url}}
26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27 |
--------------------------------------------------------------------------------
/.github/workflows/pages.yml:
--------------------------------------------------------------------------------
1 | name: Deploy documents
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | paths:
8 | - "docs/book"
9 | - ".github/workflows/pages.yml"
10 | workflow_dispatch:
11 |
12 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
13 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
14 | concurrency:
15 | group: "pages"
16 | cancel-in-progress: false
17 |
18 | jobs:
19 | # Currently nothing to build. Just uploading
20 | build:
21 | runs-on: ubuntu-latest
22 | env:
23 | HUGO_VERSION: 0.113.0
24 | steps:
25 | - name: Checkout
26 | uses: actions/checkout@v4
27 | - name: Upload artifact
28 | uses: actions/upload-pages-artifact@v3
29 | with:
30 | path: ./docs/book
31 | deploy:
32 | permissions:
33 | pages: write
34 | id-token: write
35 | environment:
36 | name: github-pages
37 | url: ${{ steps.deployment.outputs.page_url }}
38 | runs-on: ubuntu-latest
39 | needs: [build]
40 | steps:
41 | - name: Deploy to GitHub Pages
42 | id: deployment
43 | uses: actions/deploy-pages@v4
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | docsrc/node_modules
2 | docsrc/assets
3 | docsrc/_book
4 |
5 | cmd/hashirad/hashirad
6 | cmd/hashira/hashira
7 | cmd/hashira-cui/hashira-cui
8 | cmd/hashira-web-client/hashira-web-client
9 | cmd/hashira-api/hashira-api
10 | cmd/hashira-cui/github-release.sh
11 | cmd/hashira-cui/pkg
12 | .idea/
13 | vendor/
14 | dist/
15 |
16 | datastore-export-*
17 |
18 | .vscode/
19 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | linters:
3 | exclusions:
4 | generated: lax
5 | presets:
6 | - comments
7 | - common-false-positives
8 | - legacy
9 | - std-error-handling
10 | paths:
11 | - service
12 | - hashira-web/
13 | - third_party$
14 | - builtin$
15 | - examples$
16 | formatters:
17 | exclusions:
18 | generated: lax
19 | paths:
20 | - third_party$
21 | - builtin$
22 | - examples$
23 |
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | # This is an example .goreleaser.yml file with some sensible defaults.
2 | # Make sure to check the documentation at https://goreleaser.com
3 | before:
4 | hooks:
5 | # You may remove this if you don't use go modules.
6 | - go mod tidy
7 | # you may remove this if you don't need go generate
8 | - go generate ./...
9 | builds:
10 | - id: hashira-cui
11 | main: ./cmd/hashira-cui
12 | binary: hashira-cui
13 | ldflags:
14 | - -s -w
15 | - -X main.Version={{.Version}}
16 | - -X main.Revision={{.ShortCommit}}
17 | env:
18 | - CGO_ENABLED=0
19 | goarch:
20 | - amd64
21 | goos:
22 | - linux
23 | - darwin
24 | - id: hashira-web-client
25 | main: ./cmd/hashira-web-client
26 | binary: hashira-web-client
27 | ldflags:
28 | - -s -w
29 | - -X main.Version={{.Version}}
30 | - -X main.Revision={{.ShortCommit}}
31 | env:
32 | - CGO_ENABLED=0
33 | goarch:
34 | - amd64
35 | goos:
36 | - linux
37 | - darwin
38 |
39 | archives:
40 | - format: tar.gz
41 | # this name template makes the OS and Arch compatible with the results of uname.
42 | name_template: >-
43 | {{ .ProjectName }}_
44 | {{- title .Os }}_
45 | {{- if eq .Arch "amd64" }}x86_64
46 | {{- else if eq .Arch "386" }}i386
47 | {{- else }}{{ .Arch }}{{ end }}
48 | {{- if .Arm }}v{{ .Arm }}{{ end }}
49 | # use zip for windows archives
50 | format_overrides:
51 | - goos: windows
52 | format: zip
53 | checksum:
54 | name_template: "checksums.txt"
55 | snapshot:
56 | name_template: "{{ incpatch .Version }}-next"
57 | changelog:
58 | sort: asc
59 | filters:
60 | exclude:
61 | - "^docs:"
62 | - "^test:"
63 | # The lines beneath this are called `modelines`. See `:help modeline`
64 | # Feel free to remove those if you don't want/use them.
65 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json
66 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj
67 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Yosuke Akatsuka
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | MAKEFLAGS += --no-builtin-rules
3 |
4 | PROTO_DIR = proto
5 | PB_GO_DIR = service
6 |
7 | PROTOS = $(shell find $(PROTO_DIR) -type f -exec basename {} \; | grep proto$$)
8 | PB_GOS = $(PROTOS:%.proto=$(PB_GO_DIR)/%.pb.go)
9 |
10 | BUILD_CMD ?= go build
11 | UPDATE_DEPENDENCIES_CMD ?= go get -u && go mod tidy
12 |
13 | build:
14 | cd $(CURDIR)/cmd/hashira && $(BUILD_CMD)
15 | cd $(CURDIR)/cmd/hashirad && $(BUILD_CMD)
16 | cd $(CURDIR)/cmd/hashira-cui && $(BUILD_CMD)
17 | cd $(CURDIR)/cmd/hashira-web-client && $(BUILD_CMD)
18 |
19 | install:
20 | @BUILD_CMD="go install" make build
21 |
22 | all: genproto lint test
23 | make build
24 |
25 | genproto: $(PB_GOS)
26 |
27 | $(PB_GO_DIR)/%.pb.go: $(PROTO_DIR)/%.proto
28 | mkdir -p $(dir $@)
29 | protoc \
30 | -I $(PROTO_DIR) \
31 | --go_out=. \
32 | --go_opt=module=github.com/pankona/hashira \
33 | --go-grpc_out=. \
34 | --go-grpc_opt=module=github.com/pankona/hashira \
35 | ./$<
36 |
37 | update-dependencies:
38 | cd $(CURDIR)/cmd/hashira && $(UPDATE_DEPENDENCIES_CMD)
39 | cd $(CURDIR)/cmd/hashirad && $(UPDATE_DEPENDENCIES_CMD)
40 | cd $(CURDIR)/cmd/hashira-cui && $(UPDATE_DEPENDENCIES_CMD)
41 | cd $(CURDIR)/cmd/hashira-web-client && $(UPDATE_DEPENDENCIES_CMD)
42 |
43 | lint:
44 | dprint check
45 | golangci-lint run ./...
46 |
47 | test:
48 | go test -race ./...
49 |
50 | clean:
51 | rm -rf $(PB_GO_DIR)
52 |
53 | release:
54 | goreleaser --clean
55 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # hashira
2 |
3 | Application to manage **today's** tasks. Establish a Hashira for the day.\
4 | design documents are available at [here](https://pankona.github.io/hashira/)
5 |
6 | hashira on web is available here (alpha version) https://hashira-web.web.app
7 |
8 | ## Concepts
9 |
10 | - Makes tasks clear for the day and concentrate to eliminate them.
11 | - Records time consuming for each tasks to reveal differences between estimations and results.
12 | - May help increasing accuracy of our work-load estimation.
13 | - May help us to notice our waste of time.
14 |
15 | ## Features
16 |
17 | ### Manage today's tasks
18 |
19 | - "Backlogs" to add any miscellaneous tasks.
20 | - Move tasks for today to "ToDo".
21 | - Starts a task, then the task moves to "Doing".
22 | - Finishes the task, then the task moves to "Done".
23 | - If the "Doing" task is interrupted by another unexpected task, then the "Doing" task moves back to "ToDo" and new task is placed on "Doing".
24 | - At the end of the day, **(Not implemented yet)**
25 | - "Done" field is archived automatically and new one is created for new day.
26 | - "Doing" task moves back to "ToDo" automatically to intent to start again next day.
27 |
28 | ### Calculate consumed times **(Not implemented yet)**
29 |
30 | - Calculates how many times is consumed for each "Doing" task.
31 | - Consumed time is measured only for one task, which is placed on top of "Doing".
32 | - Show them in graph.
33 |
34 | ## Installation of `hashira-cui`
35 |
36 | - At this moment, only `hashira-cui` is available among hashira family.
37 | - For Android, iOS, Web, Desktop app, will be available someday...
38 | - Executable `hashira-cui` is available on [release page](https://github.com/pankona/hashira/releases) (recommended)
39 | - `hashira-cui` is `go get`able. Try following command to install `hashira-cui` via `go get`.
40 | - Note that `hashira-cui` installed via `go get` may be broken because of unsettled dependencies.
41 | - Using release page is recommended.
42 |
43 | ```bash
44 | $ go get github.com/pankona/hashira/cmd/hashira-cui
45 | ```
46 |
47 | ## Available Keybindings of hashira-cui
48 |
49 | - Ordinal use
50 |
51 | | Key | Action | Remarks |
52 | | ----- | ----------------------------------------------------------------- | ------------------------------------------------- |
53 | | Enter | Show input window for register a new task | |
54 | | e | Show input window for editing focused task | |
55 | | Space | Select focused task | |
56 | | j / k | Up/Down cursor
(change priority if a task is selected) | |
57 | | h / l | Change focused pane
(change task's pane if a task is selected) | |
58 | | i / I | Move focused task to left/right pane | |
59 | | x | Move focused task to Done | If focused task is already on Done, it is deleted |
60 |
61 | - While input
62 |
63 | | Key | Action | Remarks |
64 | | ----------- | ----------------------------------------- | ---------------------------------- |
65 | | Ctrl- b / f | Move cursor backward/forward | Same as using arrow left/right key |
66 | | Ctrl- a / e | Move cursor at start/end of line | Same as Home/End key |
67 | | Ctrl- h | Remove a character on previous of cursor | Same as Backspace |
68 | | Ctrl- d | Remove a character on cursor | Same as Delete |
69 | | Esc | Discard any change and close input window | |
70 |
71 | ## Notes
72 |
73 | - hashira generates its datastore under `$XDG_DATA_HOME/hashira`
74 | - Remove them for re-initializing hashira or leaving from using hashira...
75 |
76 | ## LICENSE
77 |
78 | MIT
79 |
80 | ## Author
81 |
82 | Yosuke Akatsuka (@pankona)
83 |
84 | - [Twitter](https://twitter.com/pankona)
85 | - [GitHub](https://github.com/pankona)
86 | - [Qiita](https://qiita.com/pankona)
87 |
--------------------------------------------------------------------------------
/client/client.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 |
8 | "github.com/pankona/hashira/service"
9 | "google.golang.org/grpc"
10 | "google.golang.org/grpc/credentials/insecure"
11 | )
12 |
13 | // Client is a hashira client structure
14 | type Client struct {
15 | Address string
16 | }
17 |
18 | func (c *Client) withClient(f func(service.HashiraClient) error) error {
19 | conn, err := grpc.NewClient(c.Address, grpc.WithTransportCredentials(insecure.NewCredentials()))
20 | if err != nil {
21 | return errors.New("failed to Dial: " + err.Error())
22 | }
23 | defer func() {
24 | e := conn.Close()
25 | if e != nil {
26 | fmt.Printf("failed to close connection: %s\n", e.Error())
27 | }
28 | }()
29 |
30 | return f(service.NewHashiraClient(conn))
31 | }
32 |
33 | // Create creates new task
34 | func (c *Client) Create(ctx context.Context, task *service.Task) error {
35 | return c.withClient(
36 | func(hc service.HashiraClient) error {
37 | com := &service.CommandCreate{
38 | Task: task,
39 | }
40 | result, err := hc.Create(ctx, com)
41 | if err != nil {
42 | return errors.New("Create failed: " + err.Error())
43 | }
44 | result.ProtoMessage()
45 | return nil
46 | })
47 | }
48 |
49 | // Update updates an existing task
50 | func (c *Client) Update(ctx context.Context, task *service.Task) error {
51 | return c.withClient(
52 | func(hc service.HashiraClient) error {
53 | com := &service.CommandUpdate{
54 | Task: task,
55 | }
56 |
57 | _, err := hc.Update(context.Background(), com)
58 | if err != nil {
59 | return fmt.Errorf("update a task failed: %s", err.Error())
60 | }
61 |
62 | return nil
63 | })
64 | }
65 |
66 | // Delete marks specified task as deleted
67 | func (c *Client) Delete(ctx context.Context, id string) error {
68 | return c.withClient(
69 | func(hc service.HashiraClient) error {
70 | com := &service.CommandDelete{
71 | Id: id,
72 | }
73 | result, err := hc.Delete(ctx, com)
74 | if err != nil {
75 | return errors.New("Delete failed: " + err.Error())
76 | }
77 | result.ProtoMessage()
78 | return nil
79 | })
80 | }
81 |
82 | // Delete marks specified task as deleted
83 | func (c *Client) PhysicalDelete(ctx context.Context, id string) error {
84 | return c.withClient(
85 | func(hc service.HashiraClient) error {
86 | com := &service.CommandPhysicalDelete{
87 | Id: id,
88 | }
89 | result, err := hc.PhysicalDelete(ctx, com)
90 | if err != nil {
91 | return errors.New("PhysicalDelete failed: " + err.Error())
92 | }
93 | result.ProtoMessage()
94 | return nil
95 | })
96 | }
97 |
98 | // Retrieve retrieves all tasks
99 | func (c *Client) Retrieve(ctx context.Context) (map[string]*service.Task, error) {
100 | var tasks map[string]*service.Task
101 |
102 | err := c.withClient(
103 | func(hc service.HashiraClient) error {
104 | com := &service.CommandRetrieve{ExcludeDeleted: true}
105 | result, err := hc.Retrieve(ctx, com)
106 | if err != nil {
107 | return errors.New("Retrieve failed: " + err.Error())
108 | }
109 | tasks = result.Tasks
110 | return nil
111 | })
112 |
113 | return tasks, err
114 | }
115 |
116 | // Retrieve retrieves all tasks
117 | func (c *Client) RetrieveAll(ctx context.Context) (map[string]*service.Task, error) {
118 | var tasks map[string]*service.Task
119 |
120 | err := c.withClient(
121 | func(hc service.HashiraClient) error {
122 | com := &service.CommandRetrieve{ExcludeDeleted: false}
123 | result, err := hc.Retrieve(ctx, com)
124 | if err != nil {
125 | return errors.New("Retrieve failed: " + err.Error())
126 | }
127 | tasks = result.Tasks
128 | return nil
129 | })
130 |
131 | return tasks, err
132 | }
133 |
134 | // UpdatePriority updates tasks' priorities
135 | func (c *Client) UpdatePriority(ctx context.Context, priorities map[string]*service.Priority) (map[string]*service.Priority, error) {
136 | var ret map[string]*service.Priority
137 |
138 | err := c.withClient(func(hc service.HashiraClient) error {
139 | com := &service.CommandUpdatePriority{
140 | Priorities: priorities,
141 | }
142 | p, err := hc.UpdatePriority(ctx, com)
143 | if err != nil {
144 | return errors.New("UpdatePriority failed: " + err.Error())
145 | }
146 | ret = p.Priorities
147 | return nil
148 | })
149 |
150 | return ret, err
151 | }
152 |
153 | // RetrievePriority retrieves tasks' priorities
154 | func (c *Client) RetrievePriority(ctx context.Context) (map[string]*service.Priority, error) {
155 | var ret map[string]*service.Priority
156 |
157 | err := c.withClient(func(hc service.HashiraClient) error {
158 | com := &service.CommandRetrievePriority{}
159 | p, err := hc.RetrievePriority(ctx, com)
160 | if err != nil {
161 | return errors.New("RetrievePriority failed: " + err.Error())
162 | }
163 | ret = p.Priorities
164 | return nil
165 | })
166 |
167 | return ret, err
168 | }
169 |
--------------------------------------------------------------------------------
/cmd/hashira-cui/ctrl.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 |
8 | "github.com/pankona/hashira/service"
9 | )
10 |
11 | // Ctrl represents controller of hashira-cui's mvc
12 | type Ctrl struct {
13 | m *Model
14 | pub Publisher
15 | queue chan delegateCommand
16 | errChan chan error
17 | }
18 |
19 | type delegateCommand struct {
20 | event delegateEvent
21 | data []interface{}
22 | }
23 |
24 | // Initialize initializes controller
25 | func (c *Ctrl) Initialize() {
26 | c.queue = make(chan delegateCommand, 128)
27 |
28 | ctx := context.Background()
29 |
30 | go func() {
31 | // TODO: support cancel using context
32 | err := <-c.errChan
33 | log.Printf("[ERROR] %v", err)
34 | }()
35 |
36 | go func() {
37 | c.eventLoop(ctx)
38 | }()
39 | }
40 |
41 | func (c *Ctrl) eventLoop(ctx context.Context) {
42 | for {
43 | // TODO: support cancel using context
44 | com := <-c.queue
45 |
46 | err := c.eventDispatch(ctx, com.event, com.data)
47 | if err != nil {
48 | c.errChan <- err
49 | }
50 |
51 | err = c.Update(context.Background())
52 | if err != nil {
53 | c.errChan <- err
54 | }
55 | }
56 | }
57 |
58 | func (c *Ctrl) eventDispatch(ctx context.Context, event delegateEvent, data []interface{}) error {
59 | var err error
60 |
61 | switch event {
62 | case AddTask:
63 | task := (*service.Task)(data[0].(*KeyedTask))
64 | task.IsDirty = true
65 |
66 | err = c.m.Create(ctx, task)
67 | case UpdateTask:
68 | task := (*service.Task)(data[0].(*KeyedTask))
69 | task.IsDirty = true
70 |
71 | err = c.m.Update(ctx, task)
72 | case DeleteTask:
73 | task := (*service.Task)(data[0].(*KeyedTask))
74 | task.IsDirty = true
75 |
76 | err = c.m.Delete(ctx, task.Id)
77 | case UpdatePriority:
78 | priority := data[0].(map[string]*service.Priority)
79 | for k := range priority {
80 | priority[k].IsDirty = true
81 | }
82 |
83 | _, err = c.m.UpdatePriority(ctx, priority)
84 | case UpdateBulk:
85 | task := (*service.Task)(data[0].(*KeyedTask))
86 | task.IsDirty = true
87 | priority := data[1].(map[string]*service.Priority)
88 | for k := range priority {
89 | priority[k].IsDirty = true
90 | }
91 |
92 | err = c.m.Update(ctx, task)
93 | if err != nil {
94 | c.errChan <- err
95 | }
96 | _, err = c.m.UpdatePriority(ctx, priority)
97 | default:
98 | panic(fmt.Sprintf("unknown delegateCommand: %v", event))
99 | }
100 |
101 | return err
102 | }
103 |
104 | // SetPublisher sets controller's Publisher
105 | func (c *Ctrl) SetPublisher(p Publisher) {
106 | c.pub = p
107 | }
108 |
109 | // Delegate is called from view to delegate functionality that are not
110 | // covered by view.
111 | func (c *Ctrl) Delegate(event delegateEvent, data ...interface{}) (err error) {
112 | c.queue <- delegateCommand{event: event, data: data}
113 | return nil
114 | }
115 |
116 | // Update retrieve latest information from model and
117 | // reflect them to view via PubSub.
118 | func (c *Ctrl) Update(ctx context.Context) error {
119 | // TODO:
120 | // List and RetrievePriority should archive in
121 | // one communication for better performance
122 | tasks, err := c.m.List(ctx)
123 | if err != nil {
124 | return err
125 | }
126 |
127 | priorities, err := c.m.RetrievePriority(ctx)
128 | if err != nil {
129 | return err
130 | }
131 |
132 | ktasks := make(map[string]*KeyedTask)
133 | for k, v := range tasks {
134 | ktasks[k] = (*KeyedTask)(v)
135 | }
136 |
137 | c.pub.Publish("update", ktasks, priorities)
138 | return nil
139 | }
140 |
--------------------------------------------------------------------------------
/cmd/hashira-cui/cursor.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | type cursor struct {
4 | index int
5 | focusedPane *Pane
6 | }
7 |
8 | func (c *cursor) sanitize(maxLen int) *cursor {
9 | ret := c
10 |
11 | if c.index < 0 {
12 | ret.index = 0
13 | } else if c.index > maxLen {
14 | ret.index = maxLen
15 | }
16 |
17 | if c.index > c.focusedPane.tasks.Len()-1 {
18 | ret.index = c.focusedPane.tasks.Len() - 1
19 | }
20 |
21 | return ret
22 | }
23 |
--------------------------------------------------------------------------------
/cmd/hashira-cui/delegater.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // Delegater in an interface to call delegate function,
4 | // to cover functionality that is not covered by view
5 | type Delegater interface {
6 | Delegate(event delegateEvent, data ...interface{}) error
7 | }
8 |
9 | type delegateEvent int
10 |
11 | const (
12 | // AddTask is a event for adding a new task
13 | AddTask delegateEvent = iota
14 | // UpdateTask is a event for updating a task
15 | UpdateTask
16 | // DeleteTask is a event for deleting a task
17 | DeleteTask
18 | // UpdatePriority is a event for updating task priority
19 | UpdatePriority
20 | // UpdateBulk updates specified task and priority
21 | UpdateBulk
22 | )
23 |
--------------------------------------------------------------------------------
/cmd/hashira-cui/editor.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | runewidth "github.com/mattn/go-runewidth"
7 | "github.com/pankona/gocui"
8 | )
9 |
10 | type hashiraEditor struct{}
11 |
12 | func (e *hashiraEditor) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
13 | switch key {
14 | case gocui.KeyCtrlF:
15 | v.MoveCursor(+1, 0, false)
16 | case gocui.KeyCtrlB:
17 | v.MoveCursor(-1, 0, false)
18 | case gocui.KeyHome, gocui.KeyCtrlA:
19 | // move to start of line
20 | _, oy := v.Origin()
21 | err := v.SetOrigin(0, oy)
22 | if err != nil {
23 | log.Printf("failed to set origin: %v", err)
24 | return
25 | }
26 | maxX, _ := v.Size()
27 | v.MoveCursor(-maxX, 0, false)
28 | case gocui.KeyEnd, gocui.KeyCtrlE:
29 | // move to end of line
30 | _, oy := v.Origin()
31 | err := v.SetOrigin(0, oy)
32 | if err != nil {
33 | log.Printf("failed to set origin: %v", err)
34 | return
35 | }
36 | bufLen := runewidth.StringWidth(v.Buffer())
37 | cx, _ := v.Cursor()
38 | v.MoveCursor(+bufLen-cx, 0, false)
39 | case gocui.KeyCtrlD:
40 | v.EditDelete(false)
41 | default:
42 | gocui.DefaultEditor.Edit(v, key, ch, mod)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/cmd/hashira-cui/keybinding.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/pankona/gocui"
5 | "github.com/pkg/errors"
6 | )
7 |
8 | const inputViewName = "input"
9 |
10 | type errContainer struct {
11 | err error
12 | }
13 |
14 | func (e *errContainer) setError(err error) {
15 | if err != nil {
16 | e.err = errors.Wrapf(e.err, "failed to set keybinding: %v", err.Error())
17 | }
18 | }
19 |
20 | // ConfigureKeyBindings configures keybindings for ordinal use
21 | func (v *View) ConfigureKeyBindings(g *gocui.Gui) error {
22 | ec := &errContainer{}
23 | for _, p := range v.panes {
24 | ec.setError(g.SetKeybinding(p.name, 'h', gocui.ModNone, v.KeyLeft))
25 | ec.setError(g.SetKeybinding(p.name, gocui.KeyArrowLeft, gocui.ModNone, v.KeyLeft))
26 | ec.setError(g.SetKeybinding(p.name, 'l', gocui.ModNone, v.KeyRight))
27 | ec.setError(g.SetKeybinding(p.name, gocui.KeyArrowRight, gocui.ModNone, v.KeyRight))
28 |
29 | ec.setError(g.SetKeybinding(p.name, 'k', gocui.ModNone, v.Up))
30 | ec.setError(g.SetKeybinding(p.name, gocui.KeyArrowUp, gocui.ModNone, v.Up))
31 | ec.setError(g.SetKeybinding(p.name, 'j', gocui.ModNone, v.Down))
32 | ec.setError(g.SetKeybinding(p.name, gocui.KeyArrowDown, gocui.ModNone, v.Down))
33 |
34 | ec.setError(g.SetKeybinding(p.name, 'x', gocui.ModNone, v.KeyDelete))
35 | ec.setError(g.SetKeybinding(p.name, gocui.KeyDelete, gocui.ModNone, v.KeyDelete))
36 | ec.setError(g.SetKeybinding(p.name, 'i', gocui.ModNone, v.KeyI))
37 | ec.setError(g.SetKeybinding(p.name, 'I', gocui.ModNone, v.KeyShiftI))
38 | ec.setError(g.SetKeybinding(p.name, 'e', gocui.ModNone, v.KeyE))
39 | ec.setError(g.SetKeybinding(p.name, gocui.KeySpace, gocui.ModNone, v.KeySpace))
40 | }
41 | ec.setError(g.SetKeybinding("", gocui.KeyEnter, gocui.ModNone, v.KeyEnter))
42 | ec.setError(g.SetKeybinding("", gocui.KeyEsc, gocui.ModNone, v.KeyEsc))
43 | ec.setError(g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, v.Quit))
44 | return ec.err
45 | }
46 |
47 | // KeyLeft reacts for "h" and left arrow
48 | func (v *View) KeyLeft(g *gocui.Gui, _ *gocui.View) error {
49 | return v.Left()
50 | }
51 |
52 | // KeyRight reacts for "l" and right arrow
53 | func (v *View) KeyRight(g *gocui.Gui, _ *gocui.View) error {
54 | return v.Right()
55 | }
56 |
57 | // KeyDelete reacts for "delete" and "x"
58 | func (v *View) KeyDelete(*gocui.Gui, *gocui.View) error {
59 | t := v.FocusedTask()
60 | return v.markTaskAsDone(t)
61 | }
62 |
63 | // KeyI reacts for "i"
64 | func (v *View) KeyI(g *gocui.Gui, gv *gocui.View) error {
65 | t := v.FocusedTask()
66 | if t == nil {
67 | return nil
68 | }
69 |
70 | if v.selectedTask != nil &&
71 | t.Id == v.selectedTask.Id {
72 | v.selectedTask = nil
73 | }
74 |
75 | return v.moveTaskTo(t, dirRight)
76 | }
77 |
78 | // KeyShiftI reacts for "I"
79 | func (v *View) KeyShiftI(g *gocui.Gui, gv *gocui.View) error {
80 | if gv.Name() == inputViewName {
81 | return v.input(g, gv)
82 | }
83 |
84 | t := v.FocusedTask()
85 | if t == nil {
86 | return nil
87 | }
88 |
89 | if v.selectedTask != nil &&
90 | t.Id == v.selectedTask.Id {
91 | v.selectedTask = nil
92 | }
93 |
94 | return v.moveTaskTo(t, dirLeft)
95 | }
96 |
97 | // KeyE reacts for "e"
98 | func (v *View) KeyE(g *gocui.Gui, gv *gocui.View) error {
99 | t := v.FocusedTask()
100 | if t == nil {
101 | return nil
102 | }
103 |
104 | v.editingTask = t
105 |
106 | return v.input(g, gv)
107 | }
108 |
109 | // KeySpace reacts for "Space"
110 | func (v *View) KeySpace(g *gocui.Gui, gv *gocui.View) error {
111 | return v.selectFocusedTask()
112 | }
113 |
114 | // KeyEnter reacts for "Enter"
115 | func (v *View) KeyEnter(g *gocui.Gui, gv *gocui.View) error {
116 | return v.input(g, gv)
117 | }
118 |
119 | // KeyEsc reacts for "Esc"
120 | func (v *View) KeyEsc(g *gocui.Gui, gv *gocui.View) error {
121 | if gv.Name() == inputViewName {
122 | return v.hideInput(g, gv)
123 | }
124 | return nil
125 | }
126 |
--------------------------------------------------------------------------------
/cmd/hashira-cui/keyedtask.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/pankona/hashira/service"
4 |
5 | // KeyedTask is service.Task with Key() function.
6 | type KeyedTask service.Task
7 |
8 | // Key returns task's ID.
9 | // This function is for satisfying orderedmap.Keyer interface.
10 | func (kt *KeyedTask) Key() string {
11 | return kt.Id
12 | }
13 |
--------------------------------------------------------------------------------
/cmd/hashira-cui/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "flag"
7 | "fmt"
8 | "io"
9 | "log"
10 | "log/syslog"
11 | "os"
12 | "os/user"
13 | "path/filepath"
14 | "time"
15 |
16 | "github.com/pankona/gocui"
17 | hashirac "github.com/pankona/hashira/client"
18 | "github.com/pankona/hashira/daemon"
19 | "github.com/pankona/hashira/database"
20 | "github.com/pankona/hashira/sync/syncutil"
21 | "github.com/pankona/hashira/xdg"
22 | )
23 |
24 | var (
25 | Version = "unset"
26 | Revision = "unset"
27 | )
28 |
29 | func main() {
30 | var (
31 | flagVersion bool
32 | )
33 | flag.BoolVar(&flagVersion, "version", false, "show version")
34 | flag.Parse()
35 |
36 | if flagVersion {
37 | fmt.Printf("hashira-cui version: %s, Revision: %s\n", Version, Revision)
38 | return
39 | }
40 |
41 | logger, err := syslog.New(syslog.LOG_INFO|syslog.LOG_LOCAL0, "hashira-cui")
42 | if err != nil {
43 | log.Printf("failed to connect to logger but continue to work: %v", err)
44 | log.SetOutput(io.Discard)
45 | } else {
46 | log.SetOutput(logger)
47 | }
48 |
49 | db, err := initializeDB()
50 | if err != nil {
51 | os.Exit(1)
52 | }
53 |
54 | const daemonPort = 50056
55 |
56 | d := &daemon.Daemon{
57 | Port: daemonPort,
58 | DB: db,
59 | }
60 |
61 | go func() {
62 | if err = d.Run(); err != nil {
63 | fmt.Printf("failed to start hashira daemon: %s\n", err.Error())
64 | os.Exit(1)
65 | }
66 | }()
67 | defer func() {
68 | d.Stop()
69 | }()
70 |
71 | // initialize gocui
72 | // specify false means: supportOverlaps = false
73 | g, err := gocui.NewGui(gocui.OutputNormal, false)
74 | if err != nil {
75 | log.Panicln(err)
76 | }
77 | defer g.Close()
78 |
79 | // prepare model
80 | hashirac := &hashirac.Client{Address: fmt.Sprintf("localhost:%d", daemonPort)}
81 | syncclient := &syncutil.Client{DaemonPort: daemonPort}
82 | m := NewModel(hashirac, syncclient)
83 |
84 | // Start synchronization with cloud if HASHIRA_ACCESS_TOKEN is set
85 | accesstoken, ok := os.LookupEnv("HASHIRA_ACCESS_TOKEN")
86 |
87 | if ok {
88 | go func() {
89 | sc := syncutil.Client{DaemonPort: daemonPort}
90 | err := sc.TestAccessToken(accesstoken)
91 | if err != nil {
92 | log.Printf("HASHIRA_ACCESSTOKEN is invalid. Synchronization is not started: %v", err)
93 | }
94 | m.SetAccessToken(accesstoken)
95 |
96 | go func() {
97 | // sync on launch immediately
98 | if err := m.SyncNow(context.Background()); err != nil {
99 | log.Printf("failed to sync: %v", err)
100 | }
101 |
102 | // start polling
103 | for {
104 | m.NotifySync()
105 | <-time.After(2 * time.Minute)
106 | }
107 | }()
108 |
109 | if err := m.SyncOnNotify(context.Background()); err != nil {
110 | log.Printf("sync on notify finished: %v", err)
111 | }
112 | }()
113 | }
114 |
115 | // prepare controller
116 | ps := &PubSub{}
117 | m.SetPublisher(ps)
118 |
119 | c := &Ctrl{
120 | m: m,
121 | pub: ps,
122 | }
123 |
124 | c.Initialize()
125 | c.SetPublisher(ps)
126 | // prepare view
127 | v := &View{}
128 | v.Initialize(g, c)
129 | g.SetManager(v)
130 |
131 | err = v.ConfigureKeyBindings(g)
132 | if err != nil {
133 | panic(fmt.Sprintf("failed to configure keybindings: %s", err.Error()))
134 | }
135 |
136 | ps.Subscribe("view", v)
137 |
138 | // retrieve tasks first for initial screen
139 | err = c.Update(context.Background())
140 | if err != nil {
141 | panic(fmt.Sprintf("failed to retrieve initial tasks: %s", err.Error()))
142 | }
143 |
144 | err = g.MainLoop()
145 | if err != nil && err != gocui.ErrQuit {
146 | log.Panicln(err)
147 | }
148 | }
149 |
150 | func initializeDB() (database.Databaser, error) {
151 | db := &database.BoltDB{}
152 | usr, err := user.Current()
153 | if err != nil {
154 | return nil, errors.New("failed to current user: " + err.Error())
155 | }
156 |
157 | legacyDataDir := filepath.Join(usr.HomeDir, ".config", "hashira")
158 | isLegacyDataDirExist := func() bool {
159 | legacyInfo, err := os.Stat(legacyDataDir)
160 | if err != nil && os.IsNotExist(err) {
161 | return false
162 | }
163 | return legacyInfo.IsDir()
164 | }
165 | dataDir := legacyDataDir
166 | if !isLegacyDataDirExist() {
167 | x := &xdg.Xdg{User: *usr}
168 | dataDir = filepath.Join(x.DataHome(), "hashira")
169 | err = os.MkdirAll(dataDir, 0700)
170 | if err != nil {
171 | return nil, errors.New("failed to create data directory: " + err.Error())
172 | }
173 | }
174 |
175 | err = db.Initialize(filepath.Join(dataDir, "db"))
176 | if err != nil {
177 | return nil, errors.New("failed to initialize db: " + err.Error())
178 | }
179 | return db, nil
180 | }
181 |
--------------------------------------------------------------------------------
/cmd/hashira-cui/model.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "log"
8 | "time"
9 |
10 | hashirac "github.com/pankona/hashira/client"
11 | "github.com/pankona/hashira/service"
12 | "github.com/pankona/hashira/sync/syncutil"
13 | )
14 |
15 | // Model represents model of hashira's mvc
16 | type Model struct {
17 | hashirac *hashirac.Client
18 | syncclient *syncutil.Client
19 |
20 | // TODO: remove if accesstoken is held by syncclient
21 | accesstoken string
22 | syncChan chan struct{}
23 |
24 | pub Publisher
25 | }
26 |
27 | func NewModel(hc *hashirac.Client, sc *syncutil.Client) *Model {
28 | return &Model{
29 | hashirac: hc,
30 | syncclient: sc,
31 | syncChan: make(chan struct{}),
32 | }
33 | }
34 |
35 | // List retrieves task list using hashira client
36 | func (m *Model) List(ctx context.Context) (map[string]*service.Task, error) {
37 | return m.hashirac.Retrieve(ctx)
38 | }
39 |
40 | // RetrievePriority retrieves priorities using hashira client
41 | func (m *Model) RetrievePriority(ctx context.Context) (map[string]*service.Priority, error) {
42 | return m.hashirac.RetrievePriority(ctx)
43 | }
44 |
45 | // UpdatePriority updates priorities using hashira client
46 | func (m *Model) UpdatePriority(ctx context.Context, p map[string]*service.Priority) (map[string]*service.Priority, error) {
47 | p, err := m.hashirac.UpdatePriority(ctx, p)
48 | if err != nil {
49 | return nil, fmt.Errorf("failed to update priority: %w", err)
50 | }
51 | m.NotifySync()
52 |
53 | return p, nil
54 | }
55 |
56 | func (m *Model) Create(ctx context.Context, task *service.Task) error {
57 | if err := m.hashirac.Create(ctx, task); err != nil {
58 | return fmt.Errorf("failed to create a new task: %w", err)
59 | }
60 | m.NotifySync()
61 |
62 | return nil
63 | }
64 |
65 | func (m *Model) Update(ctx context.Context, task *service.Task) error {
66 | if err := m.hashirac.Update(ctx, task); err != nil {
67 | return fmt.Errorf("failed to update a task: %w", err)
68 | }
69 | m.NotifySync()
70 |
71 | return nil
72 | }
73 |
74 | func (m *Model) Delete(ctx context.Context, id string) error {
75 | if err := m.hashirac.Delete(ctx, id); err != nil {
76 | return fmt.Errorf("failed to delete a task: %w", err)
77 | }
78 | m.NotifySync()
79 |
80 | return nil
81 | }
82 |
83 | func (m *Model) SetAccessToken(accesstoken string) {
84 | m.accesstoken = accesstoken
85 | }
86 |
87 | func (m *Model) SetPublisher(p Publisher) {
88 | m.pub = p
89 | }
90 |
91 | func (m *Model) NotifySync() {
92 | select {
93 | case m.syncChan <- struct{}{}:
94 | default:
95 | }
96 | }
97 |
98 | var errSyncOnNotifyCanceled = errors.New("sync on notify has been canceled")
99 |
100 | func (m *Model) SyncOnNotify(ctx context.Context) error {
101 | if m.accesstoken == "" {
102 | return nil
103 | }
104 |
105 | var cancelFunc context.CancelFunc
106 |
107 | for {
108 | select {
109 | case <-ctx.Done():
110 | return errSyncOnNotifyCanceled
111 | case <-m.syncChan:
112 | if cancelFunc != nil {
113 | cancelFunc()
114 | }
115 |
116 | ctx, cancel := context.WithCancel(ctx)
117 | cancelFunc = cancel
118 |
119 | go func(ctx context.Context) {
120 | select {
121 | case <-ctx.Done():
122 | // do nothing
123 | case <-time.After(1 * time.Minute):
124 | if err := m.sync(ctx); err != nil {
125 | log.Printf("failed to sync: %v", err)
126 | }
127 | }
128 | }(ctx)
129 | }
130 | }
131 | }
132 |
133 | func (m *Model) SyncNow(ctx context.Context) error {
134 | return m.sync(ctx)
135 | }
136 |
137 | func (m *Model) sync(ctx context.Context) error {
138 | if m.accesstoken == "" {
139 | return nil
140 | }
141 |
142 | if err := m.syncclient.Upload(m.accesstoken, syncutil.UploadDirtyOnly); err != nil {
143 | return fmt.Errorf("failed to upload tasks: %w", err)
144 | }
145 | if err := m.syncclient.Download(m.accesstoken); err != nil {
146 | return fmt.Errorf("failed to download tasks: %w", err)
147 | }
148 |
149 | // publish to notify update of tasks and priorities
150 | tasks, err := m.List(ctx)
151 | if err != nil {
152 | return fmt.Errorf("failed to retrieve tasks: %w", err)
153 | }
154 |
155 | priorities, err := m.RetrievePriority(ctx)
156 | if err != nil {
157 | return fmt.Errorf("failed to retrieve priorities: %w", err)
158 | }
159 |
160 | ktasks := make(map[string]*KeyedTask)
161 | for k, v := range tasks {
162 | ktasks[k] = (*KeyedTask)(v)
163 | }
164 |
165 | m.pub.Publish("update", ktasks, priorities)
166 |
167 | return nil
168 | }
169 |
--------------------------------------------------------------------------------
/cmd/hashira-cui/pane.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io"
6 |
7 | "github.com/pankona/gocui"
8 | "github.com/pankona/hashira/service"
9 | "github.com/pankona/orderedmap"
10 | )
11 |
12 | // Pane represents a pane,
13 | // like one of Backlog, ToDo, Doing, Done
14 | type Pane struct {
15 | name string
16 | index int // place of this pane
17 | left *Pane
18 | right *Pane
19 | place service.Place
20 | tasks *orderedmap.OrderedMap
21 | renderFrom int
22 | }
23 |
24 | type rectangle struct {
25 | x0, y0, x1, y1 int
26 | }
27 |
28 | // Layout writes tasks in pane
29 | func (p *Pane) Layout(g *gocui.Gui, c *cursor, focusedIndex int, selectedTask *KeyedTask) error {
30 | maxX, maxY := g.Size()
31 | rect := rectangle{maxX / 4 * p.index, 1, maxX/4*p.index + maxX/4 - 1, maxY - 1}
32 |
33 | v, err := g.SetView(p.name, rect.x0, rect.y0, rect.x1, rect.y1, 0)
34 | if err != nil {
35 | if err != gocui.ErrUnknownView {
36 | return err
37 | }
38 | v.Title = p.name
39 | }
40 |
41 | v.Clear()
42 |
43 | return p.render(v, rect, c, focusedIndex, selectedTask)
44 | }
45 |
46 | func (p *Pane) render(w io.Writer, rect rectangle, cursor *cursor,
47 | focusedIndex int, selectedTask *KeyedTask) error {
48 |
49 | // -1 is adjustment for considering width of frame
50 | maxLen := rect.y1 - rect.y0 - 2
51 | if maxLen < 0 {
52 | return fmt.Errorf("invalid pane height. height must be positive")
53 | }
54 |
55 | // cursor must point within pane max length
56 | c := cursor.sanitize(maxLen)
57 |
58 | // calculate index from where to render for scrolling
59 | p.renderFrom = p.calcRenderFrom(focusedIndex, maxLen)
60 |
61 | return p.renderTasks(w, c, selectedTask)
62 | }
63 |
64 | func (p *Pane) calcRenderFrom(focusedIndex, maxLen int) int {
65 | renderFrom := p.renderFrom
66 |
67 | if focusedIndex == -1 {
68 | // this pane is not focused. nop
69 | return renderFrom
70 | }
71 |
72 | // calculate renderFrom for scrolling
73 | to := renderFrom + maxLen
74 |
75 | if focusedIndex > to {
76 | renderFrom += focusedIndex - to
77 | } else if focusedIndex < p.renderFrom {
78 | renderFrom -= renderFrom - focusedIndex
79 | }
80 |
81 | return renderFrom
82 | }
83 |
84 | func (p *Pane) renderTasks(w io.Writer, cursor *cursor, selected *KeyedTask) error {
85 | var taskNum int
86 |
87 | return p.tasks.ForEach(func(i int, v orderedmap.Keyer) error {
88 | if i < p.renderFrom {
89 | // skip rendering to scroll
90 | return nil
91 | }
92 |
93 | task := v.(*KeyedTask)
94 |
95 | prefix := ""
96 | if selected != nil && task.Id == selected.Id {
97 | prefix = "*"
98 | }
99 |
100 | var err error
101 |
102 | if p == cursor.focusedPane && taskNum == cursor.index {
103 | _, err = fmt.Fprintf(w, "%s \033[3%d;%dm%s\033[0m\n", prefix, 7, 4, task.Name)
104 | } else {
105 | _, err = fmt.Fprintf(w, "%s %s\n", prefix, task.Name)
106 | }
107 |
108 | if err != nil {
109 | return fmt.Errorf("failed to render task: %v", err)
110 | }
111 |
112 | taskNum++
113 | return nil
114 | })
115 | }
116 |
--------------------------------------------------------------------------------
/cmd/hashira-cui/pubsub.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "sync"
4 |
5 | // Subscriber is an interface of PubSub subscriber
6 | type Subscriber interface {
7 | OnEvent(event string, data ...interface{})
8 | }
9 |
10 | // Publisher is an interface of PubSub publisher
11 | type Publisher interface {
12 | Publish(event string, data ...interface{})
13 | }
14 |
15 | // PubSub is a struct for pubsub message passing
16 | type PubSub struct {
17 | ss sync.Map
18 | }
19 |
20 | // Publish publishes specified event and data to subscribers
21 | func (ps *PubSub) Publish(event string, data ...interface{}) {
22 | ps.ss.Range(func(k, v interface{}) bool {
23 | s := v.(Subscriber)
24 | s.OnEvent(event, data...)
25 | return true
26 | })
27 | }
28 |
29 | // Subscribe registers subscriber to PubSub
30 | func (ps *PubSub) Subscribe(id string, s Subscriber) {
31 | ps.ss.Store(id, s)
32 | }
33 |
--------------------------------------------------------------------------------
/cmd/hashira-cui/view_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
--------------------------------------------------------------------------------
/cmd/hashira-web-client/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "flag"
6 | "fmt"
7 | "log"
8 | "os"
9 | "os/user"
10 | "path/filepath"
11 | "time"
12 |
13 | "github.com/pankona/hashira/daemon"
14 | "github.com/pankona/hashira/database"
15 | "github.com/pankona/hashira/sync/syncutil"
16 | )
17 |
18 | var (
19 | Version = "unset"
20 | Revision = "unset"
21 | )
22 |
23 | const daemonPort = 50058
24 |
25 | func main() {
26 | var (
27 | flagSync bool
28 | flagUpload bool
29 | flagDownload bool
30 | flagTest bool
31 | flagVersion bool
32 | )
33 |
34 | flag.BoolVar(&flagVersion, "version", false, "show version")
35 | flag.BoolVar(&flagSync, "sync", false, "sync (download and upload) tasks and priorities with hashira-web")
36 | flag.BoolVar(&flagUpload, "upload", false, "upload tasks and priorities to hashira-web")
37 | flag.BoolVar(&flagDownload, "download", false, "download tasks and priorities from hashira-web")
38 | flag.BoolVar(&flagTest, "test", false, "test the hashira-web works")
39 | flag.Parse()
40 |
41 | if flagVersion {
42 | fmt.Printf("hashira-web-client version: %s, Revision: %s\n", Version, Revision)
43 | return
44 | }
45 |
46 | accesstoken := os.Getenv("HASHIRA_ACCESS_TOKEN")
47 | if accesstoken == "" {
48 | log.Printf("Please specify environment variable HASHIRA_ACCESS_TOKEN. Abort.")
49 | os.Exit(1)
50 | }
51 |
52 | done := make(chan struct{})
53 | go func() {
54 | launchHashirad(done)
55 | }()
56 |
57 | time.Sleep(1 * time.Second)
58 |
59 | sc := syncutil.Client{DaemonPort: daemonPort}
60 |
61 | switch {
62 | case flagSync:
63 | if err := sc.Download(accesstoken); err != nil {
64 | log.Printf("failed to download: %v", err)
65 | }
66 | log.Printf("download completed")
67 | if err := sc.Upload(accesstoken, syncutil.UploadAll); err != nil {
68 | log.Printf("failed to upload: %v", err)
69 | }
70 | log.Printf("upload completed")
71 | case flagUpload:
72 | if err := sc.Upload(accesstoken, syncutil.UploadDirtyOnly); err != nil {
73 | log.Printf("failed to upload: %v", err)
74 | }
75 | log.Printf("upload completed")
76 | case flagDownload:
77 | if err := sc.Download(accesstoken); err != nil {
78 | log.Printf("failed to download: %v", err)
79 | }
80 | log.Printf("download completed")
81 | case flagTest:
82 | fallthrough
83 | default:
84 | if err := sc.TestAccessToken(accesstoken); err != nil {
85 | log.Printf("test access token failed: %v", err)
86 | } else {
87 | log.Println("The accesstoken is valid. hashira-web will work!")
88 | }
89 | }
90 |
91 | done <- struct{}{}
92 | }
93 |
94 | func initializeDB() (database.Databaser, error) {
95 | db := &database.BoltDB{}
96 | usr, err := user.Current()
97 | if err != nil {
98 | return nil, errors.New("failed to current user: " + err.Error())
99 | }
100 |
101 | configDir := filepath.Join(usr.HomeDir, ".config", "hashira")
102 | err = os.MkdirAll(configDir, 0700)
103 | if err != nil {
104 | return nil, errors.New("failed to create config directory: " + err.Error())
105 | }
106 |
107 | err = db.Initialize(filepath.Join(configDir, "db"))
108 | if err != nil {
109 | return nil, errors.New("failed to initialize db: " + err.Error())
110 | }
111 | return db, nil
112 | }
113 |
114 | func launchHashirad(done <-chan struct{}) {
115 | db, err := initializeDB()
116 | if err != nil {
117 | fmt.Printf("failed to initialize DB: %s\n", err.Error())
118 | os.Exit(1)
119 | }
120 |
121 | d := &daemon.Daemon{
122 | Port: daemonPort,
123 | DB: db,
124 | }
125 |
126 | go func() {
127 | <-done
128 | d.Stop()
129 | }()
130 |
131 | if err = d.Run(); err != nil {
132 | fmt.Printf("failed to start hashira daemon: %s\n", err.Error())
133 | os.Exit(1)
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/cmd/hashira/commands.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "strconv"
8 |
9 | "github.com/alecthomas/kingpin/v2"
10 | "github.com/pankona/hashira/client"
11 | "github.com/pankona/hashira/service"
12 | )
13 |
14 | func addNewCmd(ctx context.Context, c *client.Client) {
15 | newCmd := kingpin.Command(
16 | "new",
17 | "add new task with specified task name")
18 | name := newCmd.Arg(
19 | "name",
20 | "name of task which is newly added").
21 | Required().String()
22 | _ = newCmd.Action(func(pc *kingpin.ParseContext) error {
23 | return create(ctx, c, *name)
24 | })
25 | }
26 |
27 | func addListCmd(ctx context.Context, c *client.Client) {
28 | listCmd := kingpin.Command(
29 | "list",
30 | "show list of tasks")
31 | _ = listCmd.Action(func(pc *kingpin.ParseContext) error {
32 | return list(ctx, c)
33 | })
34 | }
35 |
36 | func create(ctx context.Context, c *client.Client, name string) error {
37 | t := &service.Task{
38 | Name: name,
39 | Place: service.Place_BACKLOG,
40 | }
41 | err := c.Create(ctx, t)
42 | if err != nil {
43 | return errors.New("failed to create a new task: " + err.Error())
44 | }
45 | return nil
46 | }
47 |
48 | func list(ctx context.Context, c *client.Client) error {
49 | tasks, err := c.Retrieve(ctx)
50 | if err != nil {
51 | return errors.New("failed to create a new task: " + err.Error())
52 | }
53 | for _, v := range tasks {
54 | id, err := strconv.Atoi(v.Id)
55 | if err != nil {
56 | continue
57 | }
58 | fmt.Printf("[%04d]\t%s\t%v\t%v\n", id, v.Name, v.Place, v.IsDeleted)
59 | }
60 | return nil
61 | }
62 |
--------------------------------------------------------------------------------
/cmd/hashira/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/alecthomas/kingpin/v2"
7 | "github.com/pankona/hashira/client"
8 | )
9 |
10 | const (
11 | program = "hashira"
12 | version = "v0.0.1"
13 | )
14 |
15 | func main() {
16 | c := &client.Client{
17 | Address: "localhost:50056",
18 | }
19 | ctx := context.Background()
20 |
21 | addNewCmd(ctx, c)
22 | addListCmd(ctx, c)
23 |
24 | kingpin.Version(program + " " + version)
25 | _ = kingpin.Parse()
26 | }
27 |
--------------------------------------------------------------------------------
/cmd/hashirad/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "log"
8 | "os"
9 | "os/user"
10 | "path/filepath"
11 | "time"
12 |
13 | "github.com/pankona/hashira/daemon"
14 | "github.com/pankona/hashira/database"
15 | "github.com/pankona/hashira/sync/syncutil"
16 | )
17 |
18 | func initializeDB() (database.Databaser, error) {
19 | db := &database.BoltDB{}
20 | usr, err := user.Current()
21 | if err != nil {
22 | return nil, errors.New("failed to current user: " + err.Error())
23 | }
24 |
25 | configDir := filepath.Join(usr.HomeDir, ".config", "hashira")
26 | err = os.MkdirAll(configDir, 0700)
27 | if err != nil {
28 | return nil, errors.New("failed to create config directory: " + err.Error())
29 | }
30 |
31 | err = db.Initialize(filepath.Join(configDir, "db"))
32 | if err != nil {
33 | return nil, errors.New("failed to initialize db: " + err.Error())
34 | }
35 |
36 | return db, nil
37 | }
38 |
39 | func main() {
40 | db, err := initializeDB()
41 | if err != nil {
42 | fmt.Printf("failed to initialize DB: %s\n", err.Error())
43 | os.Exit(1)
44 | }
45 |
46 | port := 50057
47 | d := &daemon.Daemon{
48 | Port: port,
49 | DB: db,
50 | }
51 |
52 | accesstoken, ok := os.LookupEnv("HASHIRA_ACCESS_TOKEN")
53 | if ok {
54 | if err := startSync(context.Background(), port, accesstoken); err != nil {
55 | log.Printf("failed to start synchronization: %v", err)
56 | }
57 | }
58 |
59 | if err = d.Run(); err != nil {
60 | fmt.Printf("failed to start hashira daemon: %s\n", err.Error())
61 | os.Exit(1)
62 | }
63 | }
64 |
65 | func startSync(ctx context.Context, daemonPort int, accesstoken string) error {
66 | fmt.Printf("start synchronization...\n")
67 |
68 | sc := syncutil.Client{DaemonPort: daemonPort}
69 | err := sc.TestAccessToken(accesstoken)
70 | if err != nil {
71 | return fmt.Errorf("HASHIRA_ACCESSTOKEN is invalid. Synchronization is not started: %w", err)
72 | }
73 | fmt.Printf("HASHIRA_ACCESSTOKEN is valid. hashira-web will work!\n")
74 |
75 | const syncInterval = 10 * time.Minute
76 |
77 | go func() {
78 | for {
79 | select {
80 | case <-ctx.Done():
81 | break
82 | default:
83 | if err := sc.Upload(accesstoken, syncutil.UploadDirtyOnly); err != nil {
84 | log.Printf("failed to upload: %v", err)
85 | }
86 | if err := sc.Download(accesstoken); err != nil {
87 | log.Printf("failed to download: %v", err)
88 | }
89 | <-time.After(syncInterval)
90 | }
91 | }
92 | }()
93 |
94 | return nil
95 | }
96 |
--------------------------------------------------------------------------------
/daemon/daemon.go:
--------------------------------------------------------------------------------
1 | package daemon
2 |
3 | import (
4 | "errors"
5 | "net"
6 | "strconv"
7 |
8 | "github.com/pankona/hashira/database"
9 | "github.com/pankona/hashira/service"
10 | "google.golang.org/grpc"
11 | "google.golang.org/grpc/reflection"
12 | )
13 |
14 | // Daemon is a structure that implement hashira service
15 | type Daemon struct {
16 | Port int
17 | DB database.Databaser
18 | server *grpc.Server
19 |
20 | service.UnimplementedHashiraServer
21 | }
22 |
23 | // Run starts hashira daemon (as gRPC server)
24 | func (d *Daemon) Run() error {
25 | p := ":" + strconv.Itoa(d.Port)
26 | listen, err := net.Listen("tcp", p)
27 | if err != nil {
28 | return errors.New("gRPC server failed to listen [" + p + "]: " + err.Error())
29 | }
30 | s := grpc.NewServer()
31 | reflection.Register(s)
32 | service.RegisterHashiraServer(s, d)
33 |
34 | d.server = s
35 |
36 | return d.server.Serve(listen)
37 | }
38 |
39 | // Stop stops hashira daemon
40 | func (d *Daemon) Stop() {
41 | d.server.Stop()
42 | }
43 |
--------------------------------------------------------------------------------
/daemon/grpc_test.go:
--------------------------------------------------------------------------------
1 | package daemon
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/pankona/hashira/database"
8 | "github.com/pankona/hashira/service"
9 | "github.com/stretchr/testify/require"
10 | )
11 |
12 | type mockDB struct {
13 | database.Databaser
14 | data map[string]map[string][]byte
15 | }
16 |
17 | func (m *mockDB) Save(bucket, id string, value []byte) (string, error) {
18 | if m.data == nil {
19 | m.data = make(map[string]map[string][]byte)
20 | }
21 |
22 | if m.data[bucket] == nil {
23 | m.data[bucket] = make(map[string][]byte)
24 | }
25 |
26 | m.data[bucket][id] = value
27 | return id, nil
28 | }
29 |
30 | func (m *mockDB) Load(bucket, id string) ([]byte, error) {
31 | if m.data == nil {
32 | return nil, nil
33 | }
34 |
35 | if m.data[bucket] == nil {
36 | return nil, nil
37 | }
38 |
39 | return m.data[bucket][id], nil
40 | }
41 |
42 | func (m *mockDB) ForEach(bucket string, f func(k, v []byte) error) error {
43 | for k, v := range m.data[bucket] {
44 | err := f([]byte(k), v)
45 | if err != nil {
46 | return err
47 | }
48 | }
49 | return nil
50 | }
51 |
52 | type testcase struct {
53 | inName string
54 | wantName string
55 | inPlace service.Place
56 | wantPlace service.Place
57 | }
58 |
59 | func testcases() []testcase {
60 | tcs := []testcase{
61 | {inName: "test", inPlace: service.Place_BACKLOG, wantName: "test", wantPlace: service.Place_BACKLOG},
62 | }
63 | return tcs
64 | }
65 |
66 | func testCreate(d *Daemon, t *testing.T) {
67 | tcs := testcases()
68 | for _, tc := range tcs {
69 | cc := &service.CommandCreate{
70 | Task: &service.Task{
71 | Name: tc.inName,
72 | Place: tc.inPlace,
73 | },
74 | }
75 | result, err := d.Create(context.Background(), cc)
76 | if err != nil {
77 | t.Fatalf("Create returned unexpected error: %s", err.Error())
78 | }
79 |
80 | if result.GetTask().GetName() != tc.wantName {
81 | t.Errorf("unexpected result. [got] %v [want] %v", result.GetTask().GetName(), tc.wantName)
82 | }
83 |
84 | if result.GetTask().GetPlace() != tc.wantPlace {
85 | t.Errorf("unexpected result. [got] %v [want] %v", result.GetTask().GetPlace(), tc.wantPlace)
86 | }
87 | }
88 | }
89 |
90 | func TestUpdatePriority(t *testing.T) {
91 | p := map[string]*service.Priority{
92 | service.Place_BACKLOG.String(): {
93 | Ids: []string{"1", "2", "3", "4", "5"},
94 | },
95 | service.Place_TODO.String(): {
96 | Ids: []string{"6", "7", "8", "9", "10"},
97 | },
98 | service.Place_DOING.String(): {
99 | Ids: []string{"11", "12", "13", "14", "15"},
100 | },
101 | service.Place_DONE.String(): {
102 | Ids: []string{"16", "17", "18", "19", "20"},
103 | },
104 | }
105 |
106 | d := &Daemon{
107 | DB: &mockDB{},
108 | }
109 | m, err := d.updatePriority(p)
110 | require.Nil(t, err)
111 |
112 | require.Equal(t, m[service.Place_BACKLOG.String()].Ids, []string{"1", "2", "3", "4", "5"})
113 | require.Equal(t, m[service.Place_TODO.String()].Ids, []string{"6", "7", "8", "9", "10"})
114 | require.Equal(t, m[service.Place_DOING.String()].Ids, []string{"11", "12", "13", "14", "15"})
115 | require.Equal(t, m[service.Place_DONE.String()].Ids, []string{"16", "17", "18", "19", "20"})
116 | }
117 |
118 | func TestEndPointCreate(t *testing.T) {
119 | d := &Daemon{
120 | DB: &mockDB{},
121 | }
122 | testCreate(d, t)
123 | }
124 |
125 | func TestEndPointRetrieve(t *testing.T) {
126 | d := &Daemon{
127 | DB: &mockDB{},
128 | }
129 | testCreate(d, t)
130 |
131 | rc := &service.CommandRetrieve{}
132 | result, err := d.Retrieve(context.Background(), rc)
133 | if err != nil {
134 | t.Fatalf("Retrieve returned unexpected error: %s", err.Error())
135 | }
136 |
137 | tcs := testcases()
138 | tasks := result.GetTasks()
139 | if len(tasks) != len(tcs) {
140 | t.Errorf("unexpected result. [got] %d [want] %d", len(tasks), len(tcs))
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/database/boltdb.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/gofrs/uuid"
7 | bolt "go.etcd.io/bbolt"
8 | )
9 |
10 | // BoltDB provides API for using BoltDB
11 | // This function implements Databaser interface.
12 | type BoltDB struct {
13 | dbpath string
14 | db *bolt.DB
15 | }
16 |
17 | func (b *BoltDB) open() error {
18 | var err error
19 | b.db, err = bolt.Open(b.dbpath, 0600, nil)
20 | if err != nil {
21 | return fmt.Errorf("failed to open database: %v", err)
22 | }
23 | return nil
24 | }
25 |
26 | func (b *BoltDB) close() error {
27 | return b.db.Close()
28 | }
29 |
30 | // Initialize initializes BoltDB instance
31 | func (b *BoltDB) Initialize(dbpath string) error {
32 | b.dbpath = dbpath
33 | err := b.open()
34 | if err != nil {
35 | return fmt.Errorf("failed to open database: %v", err)
36 | }
37 | return b.close()
38 | }
39 |
40 | // Finalize finalizes BoltDB instance
41 | func (b *BoltDB) Finalize() error {
42 | // nop
43 | return nil
44 | }
45 |
46 | func (b *BoltDB) withDBOpenClose(f func() error) error {
47 | err := b.open()
48 | if err != nil {
49 | return fmt.Errorf("open returned error: %v", err)
50 | }
51 |
52 | err = f()
53 | if err != nil {
54 | return fmt.Errorf("failed to save/load: %v", err)
55 | }
56 |
57 | return b.close()
58 | }
59 |
60 | // Save stores specified key/value to database
61 | func (b *BoltDB) Save(bucket, id string, value []byte) (string, error) {
62 | var ret string
63 | return ret, b.withDBOpenClose(
64 | func() error {
65 | return b.db.Update(
66 | func(tx *bolt.Tx) error {
67 | b, err := tx.CreateBucketIfNotExists([]byte(bucket))
68 | if err != nil {
69 | return fmt.Errorf("failed to create bucket: %v", err)
70 | }
71 | if id == "" {
72 | u, e := uuid.NewV4()
73 | if e != nil {
74 | return fmt.Errorf("failed generate uuid: %v", err)
75 | }
76 | id = u.String()
77 | }
78 |
79 | err = b.Put([]byte(id), value)
80 | if err != nil {
81 | return err
82 | }
83 | ret = id
84 | return nil
85 | })
86 | })
87 | }
88 |
89 | // Load loads data by id
90 | func (b *BoltDB) Load(bucket, id string) ([]byte, error) {
91 | var ret []byte
92 | err := b.withDBOpenClose(
93 | func() error {
94 | err := b.db.View(
95 | func(tx *bolt.Tx) error {
96 | if tx == nil {
97 | return nil
98 | }
99 | b := tx.Bucket([]byte(bucket))
100 | if b == nil {
101 | return nil
102 | }
103 | v := b.Get([]byte(id))
104 | if v == nil {
105 | return nil
106 | }
107 | ret = make([]byte, len(v))
108 | copy(ret, v)
109 | return nil
110 | })
111 | if err != nil {
112 | return fmt.Errorf("failed load: %v", err)
113 | }
114 | return nil
115 | })
116 | return ret, err
117 | }
118 |
119 | func (b *BoltDB) PhysicalDelete(bucket, id string) error {
120 | return b.withDBOpenClose(
121 | func() error {
122 | err := b.db.Update(
123 | func(tx *bolt.Tx) error {
124 | if tx == nil {
125 | return nil
126 | }
127 | b := tx.Bucket([]byte(bucket))
128 | if b == nil {
129 | return nil
130 | }
131 | if err := b.Delete([]byte(id)); err != nil {
132 | return fmt.Errorf("failed to physical delete from db: %w", err)
133 | }
134 | return nil
135 | })
136 | if err != nil {
137 | return fmt.Errorf("failed to physical delete: %v", err)
138 | }
139 | return nil
140 | })
141 | }
142 |
143 | // ForEach loops for all items already saved and invoke specified function
144 | func (b *BoltDB) ForEach(bucket string, f func(k, v []byte) error) error {
145 | err := b.withDBOpenClose(
146 | func() error {
147 | err := b.db.View(
148 | func(tx *bolt.Tx) error {
149 | if tx == nil {
150 | return nil
151 | }
152 | b := tx.Bucket([]byte(bucket))
153 | if b == nil {
154 | return nil
155 | }
156 | err := b.ForEach(f)
157 | if err != nil {
158 | return fmt.Errorf("for each stopped: %v", err)
159 | }
160 | return nil
161 | })
162 | if err != nil {
163 | return fmt.Errorf("failed load: %v", err)
164 | }
165 | return nil
166 | })
167 | return err
168 | }
169 |
--------------------------------------------------------------------------------
/database/boltdb_test.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os"
7 | "testing"
8 | )
9 |
10 | func setup(filename *string) (func(), error) {
11 | f, err := os.CreateTemp("", "")
12 | if err != nil {
13 | return nil, errors.New("fatal. failed to create tempfile")
14 | }
15 | *filename = f.Name()
16 | err = f.Close()
17 | if err != nil {
18 | return nil, errors.New("fatal. failed to close file")
19 | }
20 | return func() {
21 | err = os.RemoveAll(*filename)
22 | if err != nil {
23 | fmt.Printf("os.RemoveAll returned error: %v", err)
24 | }
25 | }, nil
26 | }
27 |
28 | func TestBoltDBInterface(t *testing.T) {
29 | var _ Databaser = &BoltDB{}
30 | }
31 |
32 | func TestBoltDBInitialize(t *testing.T) {
33 | db := &BoltDB{}
34 | var filename string
35 | teardown, err := setup(&filename)
36 | if err != nil {
37 | t.Fatalf("setup failed: %v", err)
38 | }
39 | defer teardown()
40 |
41 | err = db.Initialize(filename)
42 | if err != nil {
43 | t.Fatalf("Initialized returned unexpected error: %s", err.Error())
44 | }
45 | }
46 |
47 | func TestBoltDBFinalize(t *testing.T) {
48 | db := &BoltDB{}
49 | err := db.Finalize()
50 | if err != nil {
51 | t.Fatalf("Finalize returned unexpected error: %s", err.Error())
52 | }
53 | }
54 |
55 | func TestBoltDBSaveLoad(t *testing.T) {
56 | db := &BoltDB{}
57 | var filename string
58 | teardown, err := setup(&filename)
59 | if err != nil {
60 | t.Fatalf("setup failed: %v", err)
61 | }
62 | defer teardown()
63 |
64 | err = db.Initialize(filename)
65 | if err != nil {
66 | t.Fatalf("Initialized returned unexpected error: %s", err.Error())
67 | }
68 |
69 | _, err = db.Save("testbucket", "testid", []byte("testdata"))
70 | if err != nil {
71 | t.Fatalf("save returned unexpected error: %s", err.Error())
72 | }
73 |
74 | v, err := db.Load("testbucket", "testid")
75 | if err != nil {
76 | t.Fatalf("load returned unexpected error: %s", err.Error())
77 | }
78 |
79 | if string(v) != "testdata" {
80 | t.Fatalf("load returned unexpected result. [got] %s [want] %s", string(v), "testdata")
81 | }
82 | }
83 |
84 | func TestBoltDBSaveLoadWithoutID(t *testing.T) {
85 | db := &BoltDB{}
86 | var filename string
87 | teardown, err := setup(&filename)
88 | if err != nil {
89 | t.Fatalf("setup failed: %v", err)
90 | }
91 | defer teardown()
92 |
93 | err = db.Initialize(filename)
94 | if err != nil {
95 | t.Fatalf("Initialized returned unexpected error: %s", err.Error())
96 | }
97 |
98 | for i := 0; i < 10; i++ {
99 | _, err = db.Save("testbucket", "", []byte("testdata"))
100 | if err != nil {
101 | t.Fatalf("save returned unexpected error: %s", err.Error())
102 | }
103 | }
104 |
105 | err = db.ForEach("testbucket", func(k, v []byte) error {
106 | t.Logf("[%s] %s", string(k), string(v))
107 | return nil
108 | })
109 | if err != nil {
110 | t.Fatalf("ForEach returned unexpected error: %s", err.Error())
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/database/database.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | // Databaser is an interface to treat databases
4 | type Databaser interface {
5 | Initialize(dbpath string) error
6 | Finalize() error
7 | Save(bucket, id string, value []byte) (string, error)
8 | Load(bucket, id string) ([]byte, error)
9 | PhysicalDelete(bucket, id string) error
10 | ForEach(bucket string, f func(k, v []byte) error) error
11 | }
12 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 |
2 | all:
3 | mdbook build
4 |
--------------------------------------------------------------------------------
/docs/book.toml:
--------------------------------------------------------------------------------
1 | [book]
2 | authors = ["Yosuke Akatsuka"]
3 | language = "en"
4 | multilingual = false
5 | src = "src"
6 | title = "hashira's design document"
7 |
8 | [preprocessor.plantuml]
9 | plantuml-cmd="plantuml"
10 |
--------------------------------------------------------------------------------
/docs/book/.nojekyll:
--------------------------------------------------------------------------------
1 | This file makes sure that Github Pages doesn't process mdBook's output.
2 |
--------------------------------------------------------------------------------
/docs/book/FontAwesome/fonts/FontAwesome.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/docs/book/FontAwesome/fonts/FontAwesome.ttf
--------------------------------------------------------------------------------
/docs/book/FontAwesome/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/docs/book/FontAwesome/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/docs/book/FontAwesome/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/docs/book/FontAwesome/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/docs/book/FontAwesome/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/docs/book/FontAwesome/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/docs/book/FontAwesome/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/docs/book/FontAwesome/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/docs/book/assets/images/uml/636bfd90b359509ea12ac7f7ca3fc622a6af8f45.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/book/ayu-highlight.css:
--------------------------------------------------------------------------------
1 | /*
2 | Based off of the Ayu theme
3 | Original by Dempfi (https://github.com/dempfi/ayu)
4 | */
5 |
6 | .hljs {
7 | display: block;
8 | overflow-x: auto;
9 | background: #191f26;
10 | color: #e6e1cf;
11 | }
12 |
13 | .hljs-comment,
14 | .hljs-quote {
15 | color: #5c6773;
16 | font-style: italic;
17 | }
18 |
19 | .hljs-variable,
20 | .hljs-template-variable,
21 | .hljs-attribute,
22 | .hljs-attr,
23 | .hljs-regexp,
24 | .hljs-link,
25 | .hljs-selector-id,
26 | .hljs-selector-class {
27 | color: #ff7733;
28 | }
29 |
30 | .hljs-number,
31 | .hljs-meta,
32 | .hljs-builtin-name,
33 | .hljs-literal,
34 | .hljs-type,
35 | .hljs-params {
36 | color: #ffee99;
37 | }
38 |
39 | .hljs-string,
40 | .hljs-bullet {
41 | color: #b8cc52;
42 | }
43 |
44 | .hljs-title,
45 | .hljs-built_in,
46 | .hljs-section {
47 | color: #ffb454;
48 | }
49 |
50 | .hljs-keyword,
51 | .hljs-selector-tag,
52 | .hljs-symbol {
53 | color: #ff7733;
54 | }
55 |
56 | .hljs-name {
57 | color: #36a3d9;
58 | }
59 |
60 | .hljs-tag {
61 | color: #00568d;
62 | }
63 |
64 | .hljs-emphasis {
65 | font-style: italic;
66 | }
67 |
68 | .hljs-strong {
69 | font-weight: bold;
70 | }
71 |
72 | .hljs-addition {
73 | color: #91b362;
74 | }
75 |
76 | .hljs-deletion {
77 | color: #d96c75;
78 | }
79 |
--------------------------------------------------------------------------------
/docs/book/css/general.css:
--------------------------------------------------------------------------------
1 | /* Base styles and content styles */
2 |
3 | @import 'variables.css';
4 |
5 | :root {
6 | /* Browser default font-size is 16px, this way 1 rem = 10px */
7 | font-size: 62.5%;
8 | color-scheme: var(--color-scheme);
9 | }
10 |
11 | html {
12 | font-family: "Open Sans", sans-serif;
13 | color: var(--fg);
14 | background-color: var(--bg);
15 | text-size-adjust: none;
16 | -webkit-text-size-adjust: none;
17 | }
18 |
19 | body {
20 | margin: 0;
21 | font-size: 1.6rem;
22 | overflow-x: hidden;
23 | }
24 |
25 | code {
26 | font-family: var(--mono-font) !important;
27 | font-size: var(--code-font-size);
28 | direction: ltr !important;
29 | }
30 |
31 | /* make long words/inline code not x overflow */
32 | main {
33 | overflow-wrap: break-word;
34 | }
35 |
36 | /* make wide tables scroll if they overflow */
37 | .table-wrapper {
38 | overflow-x: auto;
39 | }
40 |
41 | /* Don't change font size in headers. */
42 | h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
43 | font-size: unset;
44 | }
45 |
46 | .left { float: left; }
47 | .right { float: right; }
48 | .boring { opacity: 0.6; }
49 | .hide-boring .boring { display: none; }
50 | .hidden { display: none !important; }
51 |
52 | h2, h3 { margin-block-start: 2.5em; }
53 | h4, h5 { margin-block-start: 2em; }
54 |
55 | .header + .header h3,
56 | .header + .header h4,
57 | .header + .header h5 {
58 | margin-block-start: 1em;
59 | }
60 |
61 | h1:target::before,
62 | h2:target::before,
63 | h3:target::before,
64 | h4:target::before,
65 | h5:target::before,
66 | h6:target::before {
67 | display: inline-block;
68 | content: "»";
69 | margin-inline-start: -30px;
70 | width: 30px;
71 | }
72 |
73 | /* This is broken on Safari as of version 14, but is fixed
74 | in Safari Technology Preview 117 which I think will be Safari 14.2.
75 | https://bugs.webkit.org/show_bug.cgi?id=218076
76 | */
77 | :target {
78 | /* Safari does not support logical properties */
79 | scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
80 | }
81 |
82 | .page {
83 | outline: 0;
84 | padding: 0 var(--page-padding);
85 | margin-block-start: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */
86 | }
87 | .page-wrapper {
88 | box-sizing: border-box;
89 | background-color: var(--bg);
90 | }
91 | .no-js .page-wrapper,
92 | .js:not(.sidebar-resizing) .page-wrapper {
93 | transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
94 | }
95 | [dir=rtl] .js:not(.sidebar-resizing) .page-wrapper {
96 | transition: margin-right 0.3s ease, transform 0.3s ease; /* Animation: slide away */
97 | }
98 |
99 | .content {
100 | overflow-y: auto;
101 | padding: 0 5px 50px 5px;
102 | }
103 | .content main {
104 | margin-inline-start: auto;
105 | margin-inline-end: auto;
106 | max-width: var(--content-max-width);
107 | }
108 | .content p { line-height: 1.45em; }
109 | .content ol { line-height: 1.45em; }
110 | .content ul { line-height: 1.45em; }
111 | .content a { text-decoration: none; }
112 | .content a:hover { text-decoration: underline; }
113 | .content img, .content video { max-width: 100%; }
114 | .content .header:link,
115 | .content .header:visited {
116 | color: var(--fg);
117 | }
118 | .content .header:link,
119 | .content .header:visited:hover {
120 | text-decoration: none;
121 | }
122 |
123 | table {
124 | margin: 0 auto;
125 | border-collapse: collapse;
126 | }
127 | table td {
128 | padding: 3px 20px;
129 | border: 1px var(--table-border-color) solid;
130 | }
131 | table thead {
132 | background: var(--table-header-bg);
133 | }
134 | table thead td {
135 | font-weight: 700;
136 | border: none;
137 | }
138 | table thead th {
139 | padding: 3px 20px;
140 | }
141 | table thead tr {
142 | border: 1px var(--table-header-bg) solid;
143 | }
144 | /* Alternate background colors for rows */
145 | table tbody tr:nth-child(2n) {
146 | background: var(--table-alternate-bg);
147 | }
148 |
149 |
150 | blockquote {
151 | margin: 20px 0;
152 | padding: 0 20px;
153 | color: var(--fg);
154 | background-color: var(--quote-bg);
155 | border-block-start: .1em solid var(--quote-border);
156 | border-block-end: .1em solid var(--quote-border);
157 | }
158 |
159 | .warning {
160 | margin: 20px;
161 | padding: 0 20px;
162 | border-inline-start: 2px solid var(--warning-border);
163 | }
164 |
165 | .warning:before {
166 | position: absolute;
167 | width: 3rem;
168 | height: 3rem;
169 | margin-inline-start: calc(-1.5rem - 21px);
170 | content: "ⓘ";
171 | text-align: center;
172 | background-color: var(--bg);
173 | color: var(--warning-border);
174 | font-weight: bold;
175 | font-size: 2rem;
176 | }
177 |
178 | blockquote .warning:before {
179 | background-color: var(--quote-bg);
180 | }
181 |
182 | kbd {
183 | background-color: var(--table-border-color);
184 | border-radius: 4px;
185 | border: solid 1px var(--theme-popup-border);
186 | box-shadow: inset 0 -1px 0 var(--theme-hover);
187 | display: inline-block;
188 | font-size: var(--code-font-size);
189 | font-family: var(--mono-font);
190 | line-height: 10px;
191 | padding: 4px 5px;
192 | vertical-align: middle;
193 | }
194 |
195 | :not(.footnote-definition) + .footnote-definition,
196 | .footnote-definition + :not(.footnote-definition) {
197 | margin-block-start: 2em;
198 | }
199 | .footnote-definition {
200 | font-size: 0.9em;
201 | margin: 0.5em 0;
202 | }
203 | .footnote-definition p {
204 | display: inline;
205 | }
206 |
207 | .tooltiptext {
208 | position: absolute;
209 | visibility: hidden;
210 | color: #fff;
211 | background-color: #333;
212 | transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
213 | left: -8px; /* Half of the width of the icon */
214 | top: -35px;
215 | font-size: 0.8em;
216 | text-align: center;
217 | border-radius: 6px;
218 | padding: 5px 8px;
219 | margin: 5px;
220 | z-index: 1000;
221 | }
222 | .tooltipped .tooltiptext {
223 | visibility: visible;
224 | }
225 |
226 | .chapter li.part-title {
227 | color: var(--sidebar-fg);
228 | margin: 5px 0px;
229 | font-weight: bold;
230 | }
231 |
232 | .result-no-output {
233 | font-style: italic;
234 | }
235 |
--------------------------------------------------------------------------------
/docs/book/css/print.css:
--------------------------------------------------------------------------------
1 |
2 | #sidebar,
3 | #menu-bar,
4 | .nav-chapters,
5 | .mobile-nav-chapters {
6 | display: none;
7 | }
8 |
9 | #page-wrapper.page-wrapper {
10 | transform: none !important;
11 | margin-inline-start: 0px;
12 | overflow-y: initial;
13 | }
14 |
15 | #content {
16 | max-width: none;
17 | margin: 0;
18 | padding: 0;
19 | }
20 |
21 | .page {
22 | overflow-y: initial;
23 | }
24 |
25 | code {
26 | direction: ltr !important;
27 | }
28 |
29 | pre > .buttons {
30 | z-index: 2;
31 | }
32 |
33 | a, a:visited, a:active, a:hover {
34 | color: #4183c4;
35 | text-decoration: none;
36 | }
37 |
38 | h1, h2, h3, h4, h5, h6 {
39 | page-break-inside: avoid;
40 | page-break-after: avoid;
41 | }
42 |
43 | pre, code {
44 | page-break-inside: avoid;
45 | white-space: pre-wrap;
46 | }
47 |
48 | .fa {
49 | display: none !important;
50 | }
51 |
--------------------------------------------------------------------------------
/docs/book/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/docs/book/favicon.png
--------------------------------------------------------------------------------
/docs/book/favicon.svg:
--------------------------------------------------------------------------------
1 |
22 |
23 |
--------------------------------------------------------------------------------
/docs/book/fonts/SOURCE-CODE-PRO-LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/docs/book/fonts/fonts.css:
--------------------------------------------------------------------------------
1 | /* Open Sans is licensed under the Apache License, Version 2.0. See http://www.apache.org/licenses/LICENSE-2.0 */
2 | /* Source Code Pro is under the Open Font License. See https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL */
3 |
4 | /* open-sans-300 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
5 | @font-face {
6 | font-family: 'Open Sans';
7 | font-style: normal;
8 | font-weight: 300;
9 | src: local('Open Sans Light'), local('OpenSans-Light'),
10 | url('open-sans-v17-all-charsets-300.woff2') format('woff2');
11 | }
12 |
13 | /* open-sans-300italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
14 | @font-face {
15 | font-family: 'Open Sans';
16 | font-style: italic;
17 | font-weight: 300;
18 | src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'),
19 | url('open-sans-v17-all-charsets-300italic.woff2') format('woff2');
20 | }
21 |
22 | /* open-sans-regular - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
23 | @font-face {
24 | font-family: 'Open Sans';
25 | font-style: normal;
26 | font-weight: 400;
27 | src: local('Open Sans Regular'), local('OpenSans-Regular'),
28 | url('open-sans-v17-all-charsets-regular.woff2') format('woff2');
29 | }
30 |
31 | /* open-sans-italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
32 | @font-face {
33 | font-family: 'Open Sans';
34 | font-style: italic;
35 | font-weight: 400;
36 | src: local('Open Sans Italic'), local('OpenSans-Italic'),
37 | url('open-sans-v17-all-charsets-italic.woff2') format('woff2');
38 | }
39 |
40 | /* open-sans-600 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
41 | @font-face {
42 | font-family: 'Open Sans';
43 | font-style: normal;
44 | font-weight: 600;
45 | src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'),
46 | url('open-sans-v17-all-charsets-600.woff2') format('woff2');
47 | }
48 |
49 | /* open-sans-600italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
50 | @font-face {
51 | font-family: 'Open Sans';
52 | font-style: italic;
53 | font-weight: 600;
54 | src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'),
55 | url('open-sans-v17-all-charsets-600italic.woff2') format('woff2');
56 | }
57 |
58 | /* open-sans-700 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
59 | @font-face {
60 | font-family: 'Open Sans';
61 | font-style: normal;
62 | font-weight: 700;
63 | src: local('Open Sans Bold'), local('OpenSans-Bold'),
64 | url('open-sans-v17-all-charsets-700.woff2') format('woff2');
65 | }
66 |
67 | /* open-sans-700italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
68 | @font-face {
69 | font-family: 'Open Sans';
70 | font-style: italic;
71 | font-weight: 700;
72 | src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'),
73 | url('open-sans-v17-all-charsets-700italic.woff2') format('woff2');
74 | }
75 |
76 | /* open-sans-800 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
77 | @font-face {
78 | font-family: 'Open Sans';
79 | font-style: normal;
80 | font-weight: 800;
81 | src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'),
82 | url('open-sans-v17-all-charsets-800.woff2') format('woff2');
83 | }
84 |
85 | /* open-sans-800italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
86 | @font-face {
87 | font-family: 'Open Sans';
88 | font-style: italic;
89 | font-weight: 800;
90 | src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'),
91 | url('open-sans-v17-all-charsets-800italic.woff2') format('woff2');
92 | }
93 |
94 | /* source-code-pro-500 - latin_vietnamese_latin-ext_greek_cyrillic-ext_cyrillic */
95 | @font-face {
96 | font-family: 'Source Code Pro';
97 | font-style: normal;
98 | font-weight: 500;
99 | src: url('source-code-pro-v11-all-charsets-500.woff2') format('woff2');
100 | }
101 |
--------------------------------------------------------------------------------
/docs/book/fonts/open-sans-v17-all-charsets-300.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/docs/book/fonts/open-sans-v17-all-charsets-300.woff2
--------------------------------------------------------------------------------
/docs/book/fonts/open-sans-v17-all-charsets-300italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/docs/book/fonts/open-sans-v17-all-charsets-300italic.woff2
--------------------------------------------------------------------------------
/docs/book/fonts/open-sans-v17-all-charsets-600.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/docs/book/fonts/open-sans-v17-all-charsets-600.woff2
--------------------------------------------------------------------------------
/docs/book/fonts/open-sans-v17-all-charsets-600italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/docs/book/fonts/open-sans-v17-all-charsets-600italic.woff2
--------------------------------------------------------------------------------
/docs/book/fonts/open-sans-v17-all-charsets-700.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/docs/book/fonts/open-sans-v17-all-charsets-700.woff2
--------------------------------------------------------------------------------
/docs/book/fonts/open-sans-v17-all-charsets-700italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/docs/book/fonts/open-sans-v17-all-charsets-700italic.woff2
--------------------------------------------------------------------------------
/docs/book/fonts/open-sans-v17-all-charsets-800.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/docs/book/fonts/open-sans-v17-all-charsets-800.woff2
--------------------------------------------------------------------------------
/docs/book/fonts/open-sans-v17-all-charsets-800italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/docs/book/fonts/open-sans-v17-all-charsets-800italic.woff2
--------------------------------------------------------------------------------
/docs/book/fonts/open-sans-v17-all-charsets-italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/docs/book/fonts/open-sans-v17-all-charsets-italic.woff2
--------------------------------------------------------------------------------
/docs/book/fonts/open-sans-v17-all-charsets-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/docs/book/fonts/open-sans-v17-all-charsets-regular.woff2
--------------------------------------------------------------------------------
/docs/book/fonts/source-code-pro-v11-all-charsets-500.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/docs/book/fonts/source-code-pro-v11-all-charsets-500.woff2
--------------------------------------------------------------------------------
/docs/book/highlight.css:
--------------------------------------------------------------------------------
1 | /*
2 | * An increased contrast highlighting scheme loosely based on the
3 | * "Base16 Atelier Dune Light" theme by Bram de Haan
4 | * (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune)
5 | * Original Base16 color scheme by Chris Kempson
6 | * (https://github.com/chriskempson/base16)
7 | */
8 |
9 | /* Comment */
10 | .hljs-comment,
11 | .hljs-quote {
12 | color: #575757;
13 | }
14 |
15 | /* Red */
16 | .hljs-variable,
17 | .hljs-template-variable,
18 | .hljs-attribute,
19 | .hljs-tag,
20 | .hljs-name,
21 | .hljs-regexp,
22 | .hljs-link,
23 | .hljs-name,
24 | .hljs-selector-id,
25 | .hljs-selector-class {
26 | color: #d70025;
27 | }
28 |
29 | /* Orange */
30 | .hljs-number,
31 | .hljs-meta,
32 | .hljs-built_in,
33 | .hljs-builtin-name,
34 | .hljs-literal,
35 | .hljs-type,
36 | .hljs-params {
37 | color: #b21e00;
38 | }
39 |
40 | /* Green */
41 | .hljs-string,
42 | .hljs-symbol,
43 | .hljs-bullet {
44 | color: #008200;
45 | }
46 |
47 | /* Blue */
48 | .hljs-title,
49 | .hljs-section {
50 | color: #0030f2;
51 | }
52 |
53 | /* Purple */
54 | .hljs-keyword,
55 | .hljs-selector-tag {
56 | color: #9d00ec;
57 | }
58 |
59 | .hljs {
60 | display: block;
61 | overflow-x: auto;
62 | background: #f6f7f6;
63 | color: #000;
64 | }
65 |
66 | .hljs-emphasis {
67 | font-style: italic;
68 | }
69 |
70 | .hljs-strong {
71 | font-weight: bold;
72 | }
73 |
74 | .hljs-addition {
75 | color: #22863a;
76 | background-color: #f0fff4;
77 | }
78 |
79 | .hljs-deletion {
80 | color: #b31d28;
81 | background-color: #ffeef0;
82 | }
83 |
--------------------------------------------------------------------------------
/docs/book/tomorrow-night.css:
--------------------------------------------------------------------------------
1 | /* Tomorrow Night Theme */
2 | /* https://github.com/jmblog/color-themes-for-highlightjs */
3 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */
4 | /* https://github.com/jmblog/color-themes-for-highlightjs */
5 |
6 | /* Tomorrow Comment */
7 | .hljs-comment {
8 | color: #969896;
9 | }
10 |
11 | /* Tomorrow Red */
12 | .hljs-variable,
13 | .hljs-attribute,
14 | .hljs-tag,
15 | .hljs-regexp,
16 | .ruby .hljs-constant,
17 | .xml .hljs-tag .hljs-title,
18 | .xml .hljs-pi,
19 | .xml .hljs-doctype,
20 | .html .hljs-doctype,
21 | .css .hljs-id,
22 | .css .hljs-class,
23 | .css .hljs-pseudo {
24 | color: #cc6666;
25 | }
26 |
27 | /* Tomorrow Orange */
28 | .hljs-number,
29 | .hljs-preprocessor,
30 | .hljs-pragma,
31 | .hljs-built_in,
32 | .hljs-literal,
33 | .hljs-params,
34 | .hljs-constant {
35 | color: #de935f;
36 | }
37 |
38 | /* Tomorrow Yellow */
39 | .ruby .hljs-class .hljs-title,
40 | .css .hljs-rule .hljs-attribute {
41 | color: #f0c674;
42 | }
43 |
44 | /* Tomorrow Green */
45 | .hljs-string,
46 | .hljs-value,
47 | .hljs-inheritance,
48 | .hljs-header,
49 | .hljs-name,
50 | .ruby .hljs-symbol,
51 | .xml .hljs-cdata {
52 | color: #b5bd68;
53 | }
54 |
55 | /* Tomorrow Aqua */
56 | .hljs-title,
57 | .css .hljs-hexcolor {
58 | color: #8abeb7;
59 | }
60 |
61 | /* Tomorrow Blue */
62 | .hljs-function,
63 | .python .hljs-decorator,
64 | .python .hljs-title,
65 | .ruby .hljs-function .hljs-title,
66 | .ruby .hljs-title .hljs-keyword,
67 | .perl .hljs-sub,
68 | .javascript .hljs-title,
69 | .coffeescript .hljs-title {
70 | color: #81a2be;
71 | }
72 |
73 | /* Tomorrow Purple */
74 | .hljs-keyword,
75 | .javascript .hljs-function {
76 | color: #b294bb;
77 | }
78 |
79 | .hljs {
80 | display: block;
81 | overflow-x: auto;
82 | background: #1d1f21;
83 | color: #c5c8c6;
84 | }
85 |
86 | .coffeescript .javascript,
87 | .javascript .xml,
88 | .tex .hljs-formula,
89 | .xml .javascript,
90 | .xml .vbscript,
91 | .xml .css,
92 | .xml .hljs-cdata {
93 | opacity: 0.5;
94 | }
95 |
96 | .hljs-addition {
97 | color: #718c00;
98 | }
99 |
100 | .hljs-deletion {
101 | color: #c82829;
102 | }
103 |
--------------------------------------------------------------------------------
/docs/src/README.md:
--------------------------------------------------------------------------------
1 | # Table of contents
2 |
3 | {{#include ./SUMMARY.md}}
4 |
--------------------------------------------------------------------------------
/docs/src/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Summary
2 |
3 | - [Introduction](README.md)
4 | - [Overview](./overview.md)
5 | - [Communication](./communication/overview.md)
6 | - [Front to daemon](./communication/front2daemon.md)
7 | - [Daemon to database](./communication/daemon2datastore.md)
8 | - [Daemon](./daemon/overview.md)
9 | - [Entity](./daemon/entity.md)
10 | - [DB tables](./tables.md)
11 | - [Prototype](./prototype.md)
12 |
--------------------------------------------------------------------------------
/docs/src/assets/images/uml/636bfd90b359509ea12ac7f7ca3fc622a6af8f45.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/src/communication/daemon2datastore.md:
--------------------------------------------------------------------------------
1 | # Communication between daemon to datastore
2 |
3 | ## Overview
4 |
5 | - Use GRPC for communication between daemon and datastore
6 | - Daemon send array of command, and datastore will apply them
7 | - Daemon retrieves updates from datastore periodically for syncing
8 |
9 | ## GRPC APIs
10 |
11 | - Use Hashira service. See [daemon overview](../daemon/overview.md).
12 |
--------------------------------------------------------------------------------
/docs/src/communication/front2daemon.md:
--------------------------------------------------------------------------------
1 | # Communication between front to daemon
2 |
3 | ## Overview
4 |
5 | - hashira uses GRPC for communication between front and daemon
6 |
7 | ## PC
8 |
9 | ### CLI and Daemon
10 |
11 | - CLI to Daemon
12 | - Add a new task to Backlog
13 | - Change task's status
14 | - Show task list
15 | - Daemon to CLI
16 | - None
17 |
18 | ### GUI and Daemon
19 |
20 | - Assume to use Electron
21 | - GUI to daemon
22 | - Add a new task to Backlog, ToDo, Doing, Done
23 | - Change task's status
24 | - Show task list on each status
25 | - Show consume of each task
26 | - Daemon to GUI
27 | - notify any update of tasks
28 |
29 | - TODO: Show GUI picture
30 |
31 | ## Android
32 |
33 | ### application and daemon
34 |
35 | - GUI to Daemon
36 | - Add a new task to Backlog, ToDo, Doing, Done
37 | - Change task's status
38 | - Show task list on each status
39 | - Show consume of each task
40 | - Daemon to GUI
41 | - Notify any update of tasks
42 |
43 | - TODO: Show GUI picture
44 |
45 | ### Widget and Daemon
46 |
47 | - Widget to Daemon
48 | - Add a new task to Backlog
49 | - Change task's status to Done
50 | - Show task list
51 | - Daemon to widget
52 | - Notify any update of tasks
53 |
54 | - TODO: Show GUI picture
55 |
--------------------------------------------------------------------------------
/docs/src/communication/overview.md:
--------------------------------------------------------------------------------
1 | # Communication
2 |
3 | - Use GRPC for communication between front and daemon
4 | - Use GRPC for communication between daemon and datastore
5 |
--------------------------------------------------------------------------------
/docs/src/daemon/entity.md:
--------------------------------------------------------------------------------
1 | # Data entities for local document store
2 |
3 | - Two data entities are declared for local document store.
4 | - Tasks
5 | - Priorities
6 |
7 | - Refer [proto file](https://github.com/pankona/hashira/blob/master/proto/hashira.proto) for latest data entity declaration.
8 |
9 | ## Tasks
10 |
11 | - Tasks represents each ToDo items.
12 |
13 | ```
14 | message Task {
15 | string id = 1;
16 | string name = 2;
17 | Place place = 3;
18 | bool isDeleted = 4;
19 | }
20 | ```
21 |
22 | ## Priorities
23 |
24 | - Priorities represents priority of each task.
25 | - Priorities are represented as its place and array of task ID.
26 | - The ID placed on lower index means higher priority.
27 |
28 | ```
29 | message Priority {
30 | Place place = 1;
31 | repeated string ids = 2;
32 | }
33 | ```
34 |
--------------------------------------------------------------------------------
/docs/src/daemon/overview.md:
--------------------------------------------------------------------------------
1 | # Overview of Daemon's functionality
2 |
3 | - Daemon has responsibility for
4 | - Receive requests from front end.
5 | - Caches commands and reflect them to local document store for working without network.
6 | - Send chunk of commands to datastore for syncing.
7 | - Receive chunk of data to sync with datastore.
8 |
9 | ## Caches data entities on local document store
10 |
11 | - Caches commands and reflect them to local document store for working without network.
12 | - [here](./entity.md) is data entities for local document store
13 |
14 | ## Sync with datastore
15 |
16 | - Daemon has a document store to cache commands from front end, and store data from datastore.
17 | - Periodically, daemon tries to perform syncing local data with datastore.
18 | - Send local commands, that are "not synced yet", to datastore.
19 | - Retrieve chunk of data. They are JSON formatted and represent "1 week of data", for example.
20 | - Daemon stores 10 weeks of data in local document store.
21 |
22 | ```plantuml
23 | @startuml
24 |
25 | hide footbox
26 |
27 | participant "client 1" as c1
28 | participant "daemon 1" as d1
29 | participant "datastore" as ds
30 | participant "daemon 2" as d2
31 | participant "client 2" as c2
32 |
33 | note right of d1 : assume daemon 1 is offline
34 | c1 -> d1 : command-1 (cached)
35 | c1 -> d1 : command-2 (cached)
36 |
37 | note right of d1 : assume daemon 1 is online,\n then send all cached commands to datastore
38 | d1 -> ds : command-1, 2 as JSON
39 |
40 | ds -> ds : apply command-1, 2
41 |
42 | d1 -> ds : fetch(10 weeks)
43 | d1 <- ds : chunk of data as JSON\n 10 weeks of data for example
44 | d1 -> d1 : store JSON to local document store
45 |
46 | note right of d2 : assume daemon 2 is offline
47 | d2 <- c2 : command-3 (cached)
48 | d2 <- c2 : command-4 (cached)
49 |
50 | note right of d2 : assume daemon 2 is online,\n then send all cached commands to datastore
51 | ds <- d2 : command-3, 4 as JSON
52 |
53 | ds -> ds : apply command-3, 4
54 |
55 | ds <- d2 : fetch(10 weeks)
56 | ds -> d2 : chunk of data as JSON
57 | d2 -> d2 : store JSON to local document store
58 |
59 | d1 -> ds : fetch(10 weeks)
60 | d1 <- ds : chunk o data as JSON
61 | d1 -> d1 : store JSON to local document store
62 |
63 | @enduml
64 | ```
65 |
66 | ## GRPC API
67 |
68 | - Hashira Service
69 | - send(array of command)
70 | - sends specified commands.
71 | - retrieve(from, to (number of weeks)) array of task
72 | - returns array of task with specified term.
73 |
74 | - command and related enumeration
75 |
76 | what (enum)
77 |
78 | | enum | remarks |
79 | | ------ | ---------------------- |
80 | | new | create a new task |
81 | | update | update state of a task |
82 |
83 | command (structure)
84 |
85 | | field | type | remarks |
86 | | ------- | ------------ | ---------------------------------------- |
87 | | what | enum of what | new, update, etc. |
88 | | payload | string | JSON formatted string how to treat what. |
89 |
90 | ## Command handling
91 |
92 | - When daemon receives commands, cache them and return ok immediately.
93 | - If daemon is online, send cached commands to datastore.
94 | - When datastore receives commands and succeed to apply them, datastore sends notification to daemon.
95 | - At daemon receiving notification, retrieve chunk of data from datastore for syncing.
96 | - When daemon succeed to apply them, daemon sends notification to front end.
97 | - At front end receiving notification, retrieve chunk of data from daemon.
98 | - When front end succeed to retrieve chunk of data, render them.
99 |
100 | - If daemon is offline, postpone to send commands to datastore.
101 | - Instead, daemon applies the cached commands to local document store,
102 | and send notification to front end as same as written above.
103 |
104 | ```plantuml
105 | @startuml
106 |
107 | hide footbox
108 |
109 | participant client as c
110 | participant daemon as d
111 | participant datastore as ds
112 |
113 | c -> d : send(command-1)
114 | d -> d : cache command-1
115 | c <- d : ok
116 |
117 | alt daemon is online
118 |
119 | note right of d : proceed on goroutine
120 | d -> ds : send(command-1)
121 | ds -> ds : apply(command-1)
122 | d <- ds : ok
123 |
124 | note right of ds: send notification\n because of update
125 | d <- ds : notify
126 |
127 | note right of d : fetch latest data\n from datastore
128 | d -> ds : fetch(10 weeks)
129 | d <- ds : chunk of data
130 | d -> d : apply data to\n local document store
131 |
132 | else daemon is offline
133 |
134 | d -> d : apply data to\n local document store\n (using cache)
135 |
136 | end
137 |
138 | note right of d : send notification\n because of update
139 | c <- d : notify
140 | c -> d : fetch(10 weeks)
141 | c <- d : chunk of data
142 | c -> c : render data
143 |
144 | @enduml
145 | ```
146 |
--------------------------------------------------------------------------------
/docs/src/overview.md:
--------------------------------------------------------------------------------
1 | # hashira Design overview
2 |
3 | ## Functions of hashira on PC
4 |
5 | ### CLI and GUI
6 |
7 | - Add a new task and place it on Backlog
8 | - Change status of tasks to ToDo, Doing, and Done
9 | - Add interrupter task, place it on Doing directory
10 | - Show list of tasks on each status
11 |
12 | ### GUI specific
13 |
14 | - Show chart to indicate how long time spent to current task
15 |
16 | ## Functions of hashira on Android
17 |
18 | ### Application
19 |
20 | - Add a new task and place it on Backlog
21 | - Change status of tasks to ToDo, Doing, and Done
22 | - Add interrupter task, place it on Doing directory
23 | - Show list of tasks on each status
24 |
25 | ### Widget
26 |
27 | - Add a new task and place it on Backlog
28 | - Show list of tasks on Backlog
29 | - Show current task and how long time spent to the task
30 |
31 | ## Functions of Daemon
32 |
33 | - It is for making hashira work even there's no network
34 | - Daemon has database to cache the tasks and "commands"
35 | - "commands" means a unit of modifications to tasks.
36 | e.g) Add new task, move task to ToDo, and so on
37 | - "commands" are used to sync with datastore
38 | - If network is available, then sync registered or changed tasks with cloud
39 |
40 | ## Functions of datastore
41 |
42 | - It is assumed to place on cloud. it is for syncing data between devices
43 | - It accepts "commands" to add or modify tasks
44 | - Daemon can retrieve chunk of commands with from-to query to reflect datastore's update to local database
45 |
46 | ## Modules
47 |
48 | ```plantuml
49 | @startuml
50 |
51 | package local {
52 | package pc {
53 | class pc_cli {
54 | }
55 | class pc_gui {
56 | }
57 | class pc_daemon {
58 | }
59 | }
60 |
61 | package android {
62 | class android_gui {
63 | }
64 | class android_daemon {
65 | }
66 | }
67 | }
68 |
69 | package cloud {
70 | class datastore {
71 | }
72 | }
73 |
74 | pc_cli --> pc_daemon
75 | pc_gui --> pc_daemon
76 | android_gui --> android_daemon
77 |
78 | pc_daemon --> datastore
79 | android_daemon --> datastore
80 |
81 | @enduml
82 | ```
83 |
--------------------------------------------------------------------------------
/docs/src/prototype.md:
--------------------------------------------------------------------------------
1 | # Prototype
2 |
3 | Here's design memorandum of hashira prototyping, for checking usability.
4 |
5 | ## Feature notice of prototype version
6 |
7 | - No performance/resource consideration
8 | - front end is for linux PC only
9 | - CUI and GUI (may use astilectron)
10 | - No cloud syncing
11 | - No "Doing" time calculation
12 | - Daemon uses MySQL as DB
13 |
14 | ## Sequence of tweaking tasks
15 |
16 | ```plantuml
17 | @startuml
18 |
19 | hide footbox
20 |
21 | actor user as user
22 | participant "cui" as cui
23 | participant "daemon" as d
24 | participant "DB" as db
25 |
26 | == initialize daemon ==
27 |
28 | user -> d : launch
29 | d -> db : create config and DB if not exist
30 | d <- db : ok
31 | d -> d : start gRPC server
32 | user <- d : ok
33 |
34 | == add a new task ==
35 |
36 | cui -> d : command(new)\n via gRPC
37 | note right of d : with task's\n name\n status\n label
38 | d -> db : execute insert query
39 | d <- db : ok
40 | cui <- d : ok
41 |
42 | == retrieve list of tasks (ordinal use) ==
43 |
44 | cui -> d : command(list)\n via gRPC
45 | note right of d : with\n Backlog\n ToDo\n Doing\n Today's Done
46 | d -> db : execute select query
47 | d <- db : ok (with task list)
48 | cui <- d : ok (with task list)
49 | cui -> cui : render using\n received tasks
50 |
51 | == retrieve list of tasks (long history) ==
52 |
53 | cui -> d : command(list)\n via gRPC
54 | note right of d : with\n Done of this week\n or this month\n or last 2 months
55 | d -> db : execute select query
56 | d <- db : ok (with task list)
57 | cui <- d : ok (with task list)
58 | cui -> cui : render using\n received tasks
59 |
60 | == move a task to Doing ==
61 |
62 | cui -> d : command(update)\n via gRPC
63 | note right of d : with\n task id\n new status (Doing)
64 | d -> db : execute update query
65 | note right of db: execute\n move current Doing to ToDo\n move specified task to Doing
66 | d <- db : ok (with updated tasks)
67 | cui <- d : ok (with updated tasks)
68 | cui -> d : retrieve list of tasks
69 | cui <- d : list of tasks
70 | cui -> cui : render using\n received tasks
71 |
72 | == move a task to Done ==
73 |
74 | cui -> d : command(update)\n via gRPC
75 | note right of d : with\n task id\n new status (Done)
76 | d -> db : execute update query
77 | note right of db: execute\n move current Doing to Done
78 | d <- db : ok (with updated task)
79 | cui <- d : ok (with updated task)
80 | cui -> d : retrieve list of tasks
81 | cui <- d : list of tasks
82 | cui -> cui : render using\n received tasks
83 |
84 | @enduml
85 | ```
86 |
--------------------------------------------------------------------------------
/docs/src/tables.md:
--------------------------------------------------------------------------------
1 | # Database tables
2 |
3 | ## User
4 |
5 | | column | type | remarks |
6 | | --------- | ------ | ----------------------------- |
7 | | user_id | number | primary key, non-null, unique |
8 | | user_name | string | non-null, unique |
9 | | password | string | non-null |
10 |
11 | ## Task
12 |
13 | | column | type | remarks |
14 | | --------- | ------ | ----------------------------- |
15 | | task_id | number | primary key, non-null, unique |
16 | | task_name | string | non-null |
17 | | user_id | number | non-null |
18 | | status_id | number | non-null |
19 | | label_id | number | non-null |
20 | | done_at | time | non-null |
21 |
22 | - task represents tasks and its status.
23 |
24 | ## Status
25 |
26 | | column | type | remarks |
27 | | ----------- | ------ | ----------------------------- |
28 | | status_id | number | primary key, non-null, unique |
29 | | status_name | string | non-null, unique |
30 |
31 | - status represents task's status. like "Backlog", "ToDo", "Doing" and "Done" will be inserted here.
32 |
33 | ## Consume
34 |
35 | | column | type | remarks |
36 | | ----------- | ------ | ----------------------------- |
37 | | consume_id | number | primary key, non-null, unique |
38 | | task_id | number | non-null |
39 | | started_at | time | non-null |
40 | | finished_at | time | non-null |
41 | | consumed | time | non-null |
42 |
43 | - consume represents how many times are consumed for specified task.
44 |
45 | ## Label
46 |
47 | | column | type | remarks |
48 | | ---------- | ------ | ----------------------------- |
49 | | label_id | number | primary key, non-null, unique |
50 | | label_name | string | non-null |
51 |
52 | - label represents task's label. this is used to task classification.
53 |
54 | ## ER diagram
55 |
56 | ```plantuml
57 | @startuml
58 |
59 | entity task {
60 | + task_id
61 | --
62 | task_name
63 | status_id
64 | label_id
65 | done_at
66 | }
67 |
68 | entity status {
69 | + status_id
70 | --
71 | status_name
72 | }
73 |
74 | entity consume {
75 | + consume_id
76 | --
77 | task_id
78 | started_at
79 | finished_at
80 | consumed
81 | }
82 |
83 | entity label {
84 | + label_id
85 | --
86 | label_name
87 | }
88 |
89 | task }---- status
90 | task }---- label
91 | task -ri-{ consume
92 |
93 | @enduml
94 | ```
95 |
--------------------------------------------------------------------------------
/dprint.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript": {},
3 | "json": {},
4 | "markdown": {},
5 | "yaml": {},
6 | "excludes": [
7 | "**/node_modules",
8 | "**/*-lock.json",
9 | "dist",
10 | "docs/book"
11 | ],
12 | "plugins": [
13 | "https://plugins.dprint.dev/typescript-0.93.3.wasm",
14 | "https://plugins.dprint.dev/json-0.19.4.wasm",
15 | "https://plugins.dprint.dev/markdown-0.17.8.wasm",
16 | "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.0.wasm"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/pankona/hashira
2 |
3 | require (
4 | github.com/alecthomas/kingpin/v2 v2.4.0
5 | github.com/gofrs/uuid v4.4.0+incompatible
6 | github.com/mattn/go-runewidth v0.0.16
7 | github.com/pankona/gocui v0.3.1-0.20190107094646-a6233010013f
8 | github.com/pankona/orderedmap v0.0.0-20190220081458-0d2a73efbdac
9 | github.com/pkg/errors v0.9.1
10 | github.com/stretchr/testify v1.10.0
11 | go.etcd.io/bbolt v1.4.0
12 | google.golang.org/grpc v1.72.2
13 | google.golang.org/protobuf v1.36.6
14 | )
15 |
16 | require (
17 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
18 | github.com/davecgh/go-spew v1.1.1 // indirect
19 | github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe // indirect
20 | github.com/kr/pretty v0.1.0 // indirect
21 | github.com/pmezard/go-difflib v1.0.0 // indirect
22 | github.com/rivo/uniseg v0.4.2 // indirect
23 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
24 | golang.org/x/net v0.38.0 // indirect
25 | golang.org/x/sys v0.31.0 // indirect
26 | golang.org/x/text v0.23.0 // indirect
27 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
28 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
29 | gopkg.in/yaml.v3 v3.0.1 // indirect
30 | )
31 |
32 | go 1.23.5
33 |
--------------------------------------------------------------------------------
/hashira-web/.firebase/hosting.cHVibGlj.cache:
--------------------------------------------------------------------------------
1 | 404.html,1632315303217,b7bab6b83fa074653ff28c8d2a64135d3434575f70a12ab3d3ba8080461b9537
2 | index.html,1632319683437,c768061a6372030d5005e4ee0a6f11812680fdd2c839a70942366bca97545863
3 | assets/bundle.js,1632321960727,aec0ba42de927fb4a95a9eb26093789d3a84883bcbeae541528521324208d8a3
4 |
--------------------------------------------------------------------------------
/hashira-web/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "hashira-web"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/hashira-web/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | public/assets
3 | build
4 | yarn-error.log
5 |
--------------------------------------------------------------------------------
/hashira-web/.tool-versions:
--------------------------------------------------------------------------------
1 | yarn 1.22.15
2 | nodejs 22.13.0
3 |
--------------------------------------------------------------------------------
/hashira-web/assets/android-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/hashira-web/assets/android-icon-144x144.png
--------------------------------------------------------------------------------
/hashira-web/assets/android-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/hashira-web/assets/android-icon-192x192.png
--------------------------------------------------------------------------------
/hashira-web/assets/android-icon-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/hashira-web/assets/android-icon-36x36.png
--------------------------------------------------------------------------------
/hashira-web/assets/android-icon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/hashira-web/assets/android-icon-48x48.png
--------------------------------------------------------------------------------
/hashira-web/assets/android-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/hashira-web/assets/android-icon-72x72.png
--------------------------------------------------------------------------------
/hashira-web/assets/android-icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/hashira-web/assets/android-icon-96x96.png
--------------------------------------------------------------------------------
/hashira-web/assets/apple-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/hashira-web/assets/apple-icon-114x114.png
--------------------------------------------------------------------------------
/hashira-web/assets/apple-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/hashira-web/assets/apple-icon-120x120.png
--------------------------------------------------------------------------------
/hashira-web/assets/apple-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/hashira-web/assets/apple-icon-144x144.png
--------------------------------------------------------------------------------
/hashira-web/assets/apple-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/hashira-web/assets/apple-icon-152x152.png
--------------------------------------------------------------------------------
/hashira-web/assets/apple-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/hashira-web/assets/apple-icon-180x180.png
--------------------------------------------------------------------------------
/hashira-web/assets/apple-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/hashira-web/assets/apple-icon-57x57.png
--------------------------------------------------------------------------------
/hashira-web/assets/apple-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/hashira-web/assets/apple-icon-60x60.png
--------------------------------------------------------------------------------
/hashira-web/assets/apple-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/hashira-web/assets/apple-icon-72x72.png
--------------------------------------------------------------------------------
/hashira-web/assets/apple-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/hashira-web/assets/apple-icon-76x76.png
--------------------------------------------------------------------------------
/hashira-web/assets/apple-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/hashira-web/assets/apple-icon-precomposed.png
--------------------------------------------------------------------------------
/hashira-web/assets/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pankona/hashira/8faa229755b2a029082f5e495e307acf9188872b/hashira-web/assets/apple-icon.png
--------------------------------------------------------------------------------
/hashira-web/assets/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |