18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/endpoints/mod.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2025 Tarin Mahmood
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 | *
6 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 | *
8 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 | */
10 |
11 |
12 | pub mod tasks;
--------------------------------------------------------------------------------
/src/backend/mod.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2025 Tarin Mahmood
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 | *
6 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 | *
8 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 | */
10 |
11 |
12 | pub mod task;
13 | pub(crate) mod serde;
--------------------------------------------------------------------------------
/src/core/mod.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2025 Tarin Mahmood
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 | *
6 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 | *
8 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 | */
10 |
11 | pub mod app;
12 | pub mod cache;
13 | pub mod config;
14 | pub mod errors;
15 | pub mod utils;
16 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "taskwarrior-web"
3 | version = "2.0.1"
4 | edition = "2024"
5 | resolver = "3"
6 |
7 | [dependencies]
8 | axum = { version = "0.8.1", features = ["multipart"] }
9 | serde = { version = "1.0.197", features = ["derive"] }
10 | tokio = { version = "1.36.0", features = ["full", "parking_lot", "tracing"] }
11 | tracing = "0.1.40"
12 | tracing-subscriber = { version = "0.3.18", features = [
13 | "env-filter",
14 | "parking_lot",
15 | ] }
16 | serde_json = "1.0.114"
17 | tera = { version = "1.20.0" }
18 | anyhow = "1.0.80"
19 | lazy_static = "1.4.0"
20 | tower = "0.5.2"
21 | tower-http = { version = "0.6.2", features = ["fs", "tracing", "trace"] }
22 | chrono = { version = "0.4.34", features = ["serde"] }
23 | csv = "1.3.1"
24 | indexmap = { version = "2.2.5", features = ["serde"] }
25 | rand = "0.9.0-beta.3"
26 | dotenvy = { version = "0.15.7" }
27 | taskchampion = { version = "2.0.3", default-features = false, features = [] }
28 | serde_path_to_error = "0.1.17"
29 | shell-words = "1.1.0"
30 | directories = "6.0.0"
31 | toml = "0.8.22"
32 | config = { version = "0.15.11", default-features = false, features = ["toml"] }
33 | linkify = "0.10.0"
34 | listenfd = "1.0.2"
35 |
36 | [dev-dependencies]
37 | tempfile = "3.19.1"
38 |
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2025 Tarin Mahmood
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 | *
6 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 | *
8 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 | */
10 |
11 | /** @type {import('tailwindcss').Config} */
12 | module.exports = {
13 | content: [
14 | "templates/*.html",
15 | "src/*.ts",
16 | ],
17 | safelist: [
18 | 'link'
19 | ]
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------
1 | name: Docker
2 |
3 | on:
4 | workflow_call:
5 | inputs:
6 | upload-tag:
7 | type: string
8 | default: "nightly"
9 | push:
10 | branches:
11 | - "main"
12 | pull_request:
13 | branches:
14 | - "main"
15 |
16 | env:
17 | REGISTRY: ghcr.io
18 | IMAGE_NAME: ${{ github.repository }}
19 |
20 | jobs:
21 | docker:
22 | runs-on: ubuntu-latest
23 | permissions:
24 | contents: read
25 | packages: write
26 | attestations: write
27 | id-token: write
28 | steps:
29 | - name: Login to Docker Hub
30 | # if: github.event_name != 'pull_request'
31 | uses: docker/login-action@v3
32 | with:
33 | registry: ${{ env.REGISTRY }}
34 | username: ${{ github.actor }}
35 | password: ${{ secrets.GITHUB_TOKEN }}
36 |
37 | - name: Extract metadata (tags, labels) for Docker
38 | id: meta
39 | uses: docker/metadata-action@v5
40 | with:
41 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
42 |
43 | - name: Build and push
44 | uses: docker/build-push-action@v6
45 | with:
46 | push: ${{ github.event_name != 'pull_request' }}
47 | tags: ${{ steps.meta.outputs.tags }}
48 | labels: ${{ steps.meta.outputs.labels }}
49 |
--------------------------------------------------------------------------------
/frontend/rollup.config.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2025 Tarin Mahmood
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 | *
6 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 | *
8 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 | */
10 |
11 | import typescript from "@rollup/plugin-typescript";
12 | import resolve from '@rollup/plugin-node-resolve';
13 | import commonjs from '@rollup/plugin-commonjs';
14 |
15 | export default {
16 | compilerOptions: {},
17 | plugins: [
18 | typescript(),
19 | resolve(),
20 | commonjs(),
21 | ],
22 | input: 'frontend/src/main.ts',
23 | output: {
24 | file: 'dist/bundle.js',
25 | }
26 | };
--------------------------------------------------------------------------------
/docker/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #
4 | # Copyright 2025 Tarin Mahmood
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7 | #
8 | # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9 | #
10 | # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 | #
12 |
13 | set -e
14 | DOTENV_FILE="$HOME/.env"
15 | # create empty dot env file
16 | echo "" > $DOTENV_FILE
17 |
18 | while IFS='=' read -r -d '' n v; do
19 | if [[ $n == TASK_WEB_* ]]; then
20 | echo "${n/TASK_WEB_/}=\"$v\"" >> $DOTENV_FILE
21 | fi
22 | done < <(env -0)
23 |
24 | # check if taskrc exists.
25 | if [[ ! -f "$TASKRC" ]]; then
26 | echo "yes" | task || true
27 | fi
28 |
29 | cd $HOME/bin
30 | exec ./taskwarrior-web &
31 | pid=$!
32 | trap 'kill -SIGTERM $pid; wait $pid' SIGTERM
33 | wait $pidh
34 |
--------------------------------------------------------------------------------
/frontend/templates/desc.html:
--------------------------------------------------------------------------------
1 | {% macro desc(task) %}
2 | {% if task.annotations %}
3 |
12 |
13 |
14 | {{ task.description | linkify | safe }}
15 |
16 |
17 | {% for annotation in task.annotations %}
18 |
35 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Sun 04, May, 2025
2 | - [x] Remember shortcuts for project and tags in a cache file.
3 | Default cache path: $HOME/.cache/taskwarrior-web/mnemonics.cache
4 |
5 | ## Thu 01, May, 2025
6 | - [x] Improved CI pipeline in order to increase continues quality checks.
7 | - [x] Improved mobile view: proper scaling of the pages as well as headers are now really sticky, not fixed anymore. This ensures, that the header does not cover any tasks on small displays.
8 |
9 | ## Tue 29, April, 2025
10 | - [x] Rework of task modifications.
11 | - [x] Adding absolute dates in task details.
12 | - [x] Added possibility to delete tasks.
13 | - [x] For denotation its now possible to select specific annotations.
14 |
15 | ## Fri 28, March, 2025
16 | - [x] Font's can be customized
17 | - [x] Removed packaged font
18 |
19 | ## Tue 04, March, 2025
20 |
21 | - [x] BIG UI UPDATE
22 | - [x] Using tailwindcss 4
23 | - [x] Using daisyui for UI components
24 | - [x] All the projects and tags are listed in single place when making selection
25 | - [x] Dialog boxes are now using default dialog element instead of hacked out modal windows
26 | - [x] Tag selection, Task selection UI should be more responsive
27 |
28 | ## Tue 15, 2024
29 |
30 | - [x] Added time of the day widget
31 | - [x] Project is now split by `.` in the tag bar
32 |
33 | ## Sat 07, 2024
34 | - [1412]
35 | - [x] Top headers are now sticky, so when displaying a long list of tasks and executing any action do not jump to top
36 | - [x] Notification is more visible
37 | - [x] UI is a little bit polished, and cleaner, small tweaks in spacing
38 | - [2125]
39 | - [x] Fixed the way toast is displayed,
40 | - [x] Escape key to close the toast
41 | - [x] Clicking on empty space focuses Cmd Bar
42 |
43 | ## Mon 29, 2024 03:05
44 | - [x] Highlighting due column of a task if due
45 |
46 | ## Mon 29, 2024
47 | - [x] [BUG] Mnemonic tag in the last row in task selection mode getting cut
48 |
49 | ## Fri 26 and older
50 | - [x] Now using GitHub action to create release builds
51 | - [x] Marking a task done with keyboard shortcut
52 | - [x] Bug fix, not unmarking completed task
53 | - [x] Modification
54 | - [x] Editing
55 | - [x] Stopping active task from List
56 | - [x] Starting tasks
57 | - [x] Annotation/Denotation
58 | - [x] Error handling
59 | - [x] Returning error message in case error occurred
60 | - [x] Which port to run
61 |
--------------------------------------------------------------------------------
/frontend/templates/active_task.html:
--------------------------------------------------------------------------------
1 |
10 |
11 |
35 | {% for tag, shortcut in tags_map %}
36 | {% if tag is keyword_tag %}
37 | {% endif %}
38 | {% if tag is user_tag %}
39 | {% endif %}
40 |
41 |
42 |
60 |
61 |
62 | {% endfor %}
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/endpoints/tasks/task_query_builder/tests.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2025 Tarin Mahmood
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 | *
6 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 | *
8 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 | */
10 |
11 |
12 | use super::*;
13 | use crate::endpoints::tasks::read_task_file;
14 |
15 | #[test]
16 | fn modifying_existing_task_query() {
17 | let p = TWGlobalState {
18 | query: Some("priority:H".to_string()),
19 | report: None,
20 | ..TWGlobalState::default()
21 | };
22 | let mut task_query = TaskQuery::new(p);
23 | task_query.update(TWGlobalState {
24 | report: None,
25 | status: Some("pending".to_string()),
26 | ..TWGlobalState::default()
27 | });
28 | assert_eq!(
29 | &task_query.as_filter_text().join(" "),
30 | "priority:H status:pending"
31 | )
32 | }
33 |
34 | #[test]
35 | fn with_priority_string_with_status() {
36 | let p = TWGlobalState {
37 | query: Some("priority:H".to_string()),
38 | report: None,
39 | status: Some("pending".to_string()),
40 | ..TWGlobalState::default()
41 | };
42 | let task_query = TaskQuery::new(p);
43 | assert_eq!(
44 | &task_query.as_filter_text().join(" "),
45 | "priority:H status:pending"
46 | )
47 | }
48 |
49 | #[test]
50 | fn with_priority_string_with_no_status() {
51 | let p = TWGlobalState {
52 | query: Some("priority:H".to_string()),
53 | report: None,
54 | ..TWGlobalState::default()
55 | };
56 | let task_query = TaskQuery::new(p);
57 | assert_eq!(&task_query.as_filter_text().join(" "), "priority:H next")
58 | }
59 |
60 | #[test]
61 | fn with_empty_search_param() {
62 | let p = TWGlobalState {
63 | report: None,
64 | ..TWGlobalState::default()
65 | };
66 | let task_query = TaskQuery::new(p);
67 | assert_eq!(&task_query.as_filter_text().join(" "), "next")
68 | }
69 |
70 | #[test]
71 | fn when_containing_status() {
72 | let p = TWGlobalState {
73 | report: None,
74 | status: Some("completed".to_string()),
75 | ..TWGlobalState::default()
76 | };
77 | let query = TaskQuery::new(p).as_filter_text();
78 | assert_eq!(&query.join(" "), "status:completed")
79 | }
80 |
81 | #[test]
82 | fn task_by_uuid() {
83 | let mut p = TWGlobalState::default();
84 | let test_uuid = "794618dd-7a41-4aca-ab2e-70cc4a04b5e6".to_string();
85 | p.filter = Some(test_uuid);
86 | let t = TaskQuery::new(p);
87 | println!("{:?}", t);
88 | println!("{:?}", t.as_filter_text());
89 | let tasks = read_task_file(&t).unwrap();
90 | println!("{:#?}", tasks);
91 | }
92 |
--------------------------------------------------------------------------------
/src/core/utils.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2025 Tarin Mahmood
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 | *
6 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 | *
8 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 | */
10 |
11 | use rand::distr::{Alphanumeric, SampleString};
12 | use std::collections::HashSet;
13 | use tracing::{error, trace};
14 |
15 | use super::{app::AppState, cache::MnemonicsType};
16 |
17 | pub fn make_shortcut(shortcuts: &mut HashSet) -> String {
18 | let alpha = Alphanumeric::default();
19 | let mut len = 2;
20 | let mut tries = 0;
21 | loop {
22 | let shortcut = alpha.sample_string(&mut rand::rng(), len).to_lowercase();
23 | if !shortcuts.contains(&shortcut) {
24 | shortcuts.insert(shortcut.clone());
25 | return shortcut;
26 | }
27 | tries += 1;
28 | if tries > 1000 {
29 | len += 1;
30 | if len > 3 {
31 | panic!("too many shortcuts! this should not happen");
32 | }
33 | tries = 0;
34 | }
35 | }
36 | }
37 |
38 | pub fn make_shortcut_cache(mn_type: MnemonicsType, key: &str, app_state: &AppState) -> String {
39 | let alpha = Alphanumeric::default();
40 | let mut len = 2;
41 | let mut tries = 0;
42 | // Check if available in cache.
43 | let shortcut_cache = app_state
44 | .app_cache
45 | .read()
46 | .unwrap()
47 | .get(mn_type.clone(), key);
48 | if let Some(shortcut_cache) = shortcut_cache {
49 | return shortcut_cache;
50 | }
51 |
52 | loop {
53 | let shortcut = alpha.sample_string(&mut rand::rng(), len).to_lowercase();
54 | let shortcut_insert =
55 | app_state
56 | .app_cache
57 | .write()
58 | .unwrap()
59 | .insert(mn_type.clone(), key, &shortcut, false);
60 | if shortcut_insert.is_ok() {
61 | trace!(
62 | "Searching shortcut for type {:?} with key {} and found {}",
63 | &mn_type,
64 | key,
65 | &shortcut
66 | );
67 | return shortcut;
68 | } else {
69 | error!(
70 | "Failed generating and saving shortcut {} for type {:?} - error: {:?}",
71 | shortcut,
72 | &mn_type,
73 | shortcut_insert.err()
74 | );
75 | }
76 | tries += 1;
77 | if tries > 1000 {
78 | len += 1;
79 | if len > 3 {
80 | panic!("too many shortcuts! this should not happen");
81 | }
82 | tries = 0;
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/frontend/src/theme.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2025 Tarin Mahmood
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 | *
6 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 | *
8 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 | */
10 |
11 | export const SUPPORTED_THEMES = ["taskwarrior-dark", "taskwarrior-light"];
12 | export const THEME_ICONS = ["⚹", "☽", "🌣"];
13 | const STORAGE_THEME_KEY = "TWK_THEME";
14 | const DOM_THEME_KEY = "data-theme";
15 |
16 | function getThemeStorage() : string | null {
17 | const theme = localStorage.getItem(STORAGE_THEME_KEY);
18 | return theme;
19 | }
20 |
21 | function getThemeDom() : string | null {
22 | const theme = document.getElementsByTagName('html')[0].getAttribute(DOM_THEME_KEY);
23 | return theme;
24 | }
25 |
26 | function getTheme() : string | null {
27 | const themeStorage = getThemeStorage();
28 | const themeDom = getThemeDom();
29 |
30 | return themeDom === null ? themeStorage : themeDom;
31 | }
32 |
33 | function setTheme(theme: string | null, overrideStorage: boolean = true) : boolean {
34 | if (theme === null) {
35 | if (overrideStorage) {
36 | localStorage.removeItem(STORAGE_THEME_KEY);
37 | }
38 | document.getElementsByTagName('html')[0].removeAttribute(DOM_THEME_KEY);
39 | } else {
40 | if (overrideStorage) {
41 | localStorage.setItem(STORAGE_THEME_KEY, theme);
42 | }
43 | document.getElementsByTagName('html')[0].setAttribute(DOM_THEME_KEY, theme);
44 | }
45 |
46 | let themeIndex = -1;
47 | if (theme != null) {
48 | themeIndex = SUPPORTED_THEMES.indexOf(theme);
49 | }
50 | const iconIndex = themeIndex + 1;
51 | document.getElementById('theme-switcher')?.innerText = THEME_ICONS.at(iconIndex);
52 |
53 | return true;
54 | }
55 |
56 | export function switchTheme() {
57 | const currentTheme = getTheme();
58 | let themeIndex = -1;
59 | if (currentTheme != null) {
60 | themeIndex = SUPPORTED_THEMES.indexOf(currentTheme);
61 | }
62 | themeIndex = themeIndex + 1;
63 | if (themeIndex >= SUPPORTED_THEMES.length) {
64 | themeIndex = -1;
65 | }
66 |
67 | if (themeIndex >= 0) {
68 | setTheme(SUPPORTED_THEMES.at(themeIndex)!);
69 | } else {
70 | setTheme(null);
71 | }
72 | }
73 |
74 | export function init() {
75 | // If a theme is already set on storage, force it!
76 | const theme = getThemeStorage();
77 | if (theme != null) {
78 | setTheme(theme!);
79 | return;
80 | }
81 |
82 | // Ensure, that the icon is set correctly.
83 | // Do not override the storage!
84 | // This part is only done, if nothing is given yet in storage.
85 | const themeDom = getThemeDom();
86 | setTheme(themeDom!, false);
87 | }
--------------------------------------------------------------------------------
/frontend/css/style.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2025 Tarin Mahmood
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 | *
6 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 | *
8 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 | */
10 |
11 | @import "tailwindcss";
12 | @config "../tailwind.config.js";
13 | @plugin "daisyui" {
14 | themes: taskwarrior-light, taskwarrior-dark;
15 | root: ":root";
16 | darktheme: "taskwarrior-dark";
17 | logs: false;
18 | }
19 |
20 | @plugin "daisyui/theme" {
21 | name: "taskwarrior-dark";
22 | default: false;
23 | prefersdark: true;
24 | color-scheme: "dark";
25 | --color-base-100: oklch(21% 0.006 56.043);
26 | --color-base-200: oklch(14% 0.004 49.25);
27 | --color-base-300: oklch(0% 0 0);
28 | --color-base-content: oklch(84.955% 0 0);
29 | --color-primary: oklch(50% 0.134 242.749);
30 | --color-primary-content: oklch(19.693% 0.004 196.779);
31 | --color-secondary: oklch(43% 0 0);
32 | --color-secondary-content: oklch(89.196% 0.049 305.03);
33 | --color-accent: oklch(55% 0.027 264.364);
34 | --color-accent-content: oklch(0% 0 0);
35 | --color-neutral: oklch(37% 0.034 259.733);
36 | --color-neutral-content: oklch(84.874% 0.009 65.681);
37 | --color-info: oklch(54.615% 0.215 262.88);
38 | --color-info-content: oklch(90.923% 0.043 262.88);
39 | --color-success: oklch(62.705% 0.169 149.213);
40 | --color-success-content: oklch(12.541% 0.033 149.213);
41 | --color-warning: oklch(66.584% 0.157 58.318);
42 | --color-warning-content: oklch(13.316% 0.031 58.318);
43 | --color-error: oklch(65.72% 0.199 27.33);
44 | --color-error-content: oklch(13.144% 0.039 27.33);
45 | --radius-selector: 0.25rem;
46 | --radius-field: 0.25rem;
47 | --radius-box: 0.25rem;
48 | --size-selector: 0.25rem;
49 | --size-field: 0.25rem;
50 | --border: 1px;
51 | --depth: 1;
52 | --noise: 0;
53 | }
54 |
55 | @plugin "daisyui/theme" {
56 | name: "taskwarrior-light";
57 | default: false;
58 | prefersdark: false;
59 | color-scheme: "light";
60 | --color-base-100: oklch(96% 0.059 95.617);
61 | --color-base-200: oklch(88.272% 0.049 91.774);
62 | --color-base-300: oklch(84.133% 0.065 90.856);
63 | --color-base-content: oklch(44% 0.011 73.639);
64 | --color-primary: oklch(52% 0.105 223.128);
65 | --color-primary-content: oklch(97% 0.013 236.62);
66 | --color-secondary: oklch(92% 0.084 155.995);
67 | --color-secondary-content: oklch(44% 0.119 151.328);
68 | --color-accent: oklch(68% 0.162 75.834);
69 | --color-accent-content: oklch(98% 0.022 95.277);
70 | --color-neutral: oklch(44% 0.011 73.639);
71 | --color-neutral-content: oklch(86% 0.005 56.366);
72 | --color-info: oklch(58% 0.158 241.966);
73 | --color-info-content: oklch(96% 0.059 95.617);
74 | --color-success: oklch(51% 0.096 186.391);
75 | --color-success-content: oklch(96% 0.059 95.617);
76 | --color-warning: oklch(64% 0.222 41.116);
77 | --color-warning-content: oklch(96% 0.059 95.617);
78 | --color-error: oklch(70% 0.191 22.216);
79 | --color-error-content: oklch(40% 0.123 38.172);
80 | --radius-selector: 0.25rem;
81 | --radius-field: 0.25rem;
82 | --radius-box: 0.25rem;
83 | --size-selector: 0.25rem;
84 | --size-field: 0.25rem;
85 | --border: 1px;
86 | --depth: 1;
87 | --noise: 0;
88 | }
89 |
90 | @plugin "@tailwindcss/typography";
91 | @plugin "@tailwindcss/forms";
92 |
93 | .shortcut_key {
94 | @apply px-1 mx-0.5 space-x-0 text-base-content bg-base-100 rounded-sm ;
95 | }
96 |
--------------------------------------------------------------------------------
/frontend/templates/undo_report.html:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
{{ heading }}
13 |
14 | The undo command is not reversible. Are you sure you want to revert to the previous state?
15 |
16 |
17 |
18 | {% for kv, vv in undo_report %}
19 | {% for operation in vv %}
20 | {% if loop.first %}
21 |
22 |
{{ kv }}
23 |
24 |
25 |
26 |
Operation
27 |
Change
28 |
29 | {% endif %}
30 |
31 |
32 |
{{ operation.operation }}
33 |
{% if operation.property %}
34 | {% if operation.is_tag_change %}
35 | {% if operation.old_value %}
36 | Removed tag {{ operation.property }}
37 | {% else %}
38 | Added tag {{ operation.property }}
39 | {% endif %}
40 | {% else %}
41 | Attribute: {{ operation.property }}
42 | {% if operation.old_value %}
43 | Old value: {{ operation.old_value }}
44 | {% endif %}
45 | New value: {{ operation.value }}
46 | {% endif %}
47 | {% endif %}
48 |
49 | {% if operation.old_task %}
50 | Following attributes were set:
51 | {% for k,v in operation.old_task %}
52 | {{ k }} = {{ v }}
53 | {% endfor %}
54 | {% endif %}
55 |
56 |
57 | {% endfor %}
58 | {% endfor %}
59 |
60 |
61 |
62 |
63 |
71 |
72 |
79 |
80 |
83 |
84 |
--------------------------------------------------------------------------------
/src/core/errors.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2025 Tarin Mahmood
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 | *
6 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 | *
8 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 | */
10 |
11 | use std::collections::HashMap;
12 |
13 | use axum::{
14 | http::StatusCode,
15 | response::{IntoResponse, Response},
16 | };
17 | use serde::{Deserialize, Serialize};
18 |
19 | pub struct AppError(anyhow::Error);
20 |
21 | // Tell axum how to convert `AppError` into a response.
22 | impl IntoResponse for AppError {
23 | fn into_response(self) -> Response {
24 | (
25 | StatusCode::INTERNAL_SERVER_ERROR,
26 | format!("Something went wrong: {}", self.0),
27 | )
28 | .into_response()
29 | }
30 | }
31 |
32 | // This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
33 | // `Result<_, AppError>`. That way you don't need to do that manually.
34 | impl From for AppError
35 | where
36 | E: Into,
37 | {
38 | fn from(err: E) -> Self {
39 | Self(err.into())
40 | }
41 | }
42 |
43 | impl ToString for AppError {
44 | fn to_string(&self) -> String {
45 | self.0.to_string()
46 | }
47 | }
48 |
49 | #[derive(Clone, Debug, Serialize, Deserialize)]
50 | pub struct FieldError {
51 | pub field: String,
52 | pub message: String,
53 | }
54 |
55 | #[derive(Clone, Debug, Serialize, Deserialize)]
56 | pub struct FormValidation {
57 | pub fields: HashMap>,
58 | pub msg: Option,
59 | success: bool,
60 | }
61 |
62 | impl Default for FormValidation {
63 | fn default() -> Self {
64 | Self {
65 | fields: Default::default(),
66 | msg: None,
67 | success: true,
68 | }
69 | }
70 | }
71 |
72 | impl std::fmt::Display for FormValidation {
73 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 | let field_list: Vec = self.fields.values().map(|f| {
75 | let msg_list: Vec = f.iter().map(|x| format!("{}={}", x.field.to_string(), x.message.to_string())).collect();
76 | msg_list.join(", ")
77 | }).collect();
78 | write!(f, "FormValidation error was {:?}, affected fields: {}", self.success, field_list.join("; "))
79 | }
80 | }
81 |
82 | impl std::error::Error for FormValidation {
83 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
84 | None
85 | }
86 |
87 | fn description(&self) -> &str {
88 | "description() is deprecated; use Display"
89 | }
90 |
91 | fn cause(&self) -> Option<&dyn std::error::Error> {
92 | self.source()
93 | }
94 | }
95 |
96 | impl From for FormValidation {
97 | fn from(value: anyhow::Error) -> Self {
98 | Self::default().set_error(Some(&value.to_string())).to_owned()
99 | }
100 | }
101 |
102 | impl From for FormValidation {
103 | fn from(value: taskchampion::Error) -> Self {
104 | Self::default().set_error(Some(&value.to_string())).to_owned()
105 | }
106 | }
107 |
108 | impl FormValidation {
109 | pub fn push(&mut self, error: FieldError) -> () {
110 | self.success = false;
111 | if let Some(val) = self.fields.get_mut(&error.field) {
112 | val.push(error);
113 | } else {
114 | self.fields.insert(error.field.to_string(), vec![error]);
115 | }
116 | }
117 |
118 | /// Check if any validation errors occured or if no errors were recognized.
119 | /// If everything went fine, `is_success` returns `true`.
120 | pub fn is_success(&self) -> bool {
121 | self.success
122 | }
123 |
124 | pub fn set_error(&mut self, msg: Option<&str>) -> &Self {
125 | if let Some(err_msg) = msg {
126 | self.success = false;
127 | self.msg = Some(err_msg.to_string());
128 | } else {
129 | self.success = !self.fields.is_empty();
130 | self.msg = None;
131 | }
132 |
133 | self
134 | }
135 |
136 | /// Checks whether errors occured for given `field`.
137 | /// If at least one error to the given `field`, a `true`
138 | /// is returned.
139 | pub fn has_error(&self, field: &str) -> bool {
140 | self.fields.contains_key(field)
141 | }
142 |
143 | }
144 |
--------------------------------------------------------------------------------
/src/backend/serde.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2025 Tarin Mahmood
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 | *
6 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 | *
8 | * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 | */
10 |
11 |
12 |
13 | pub(crate) mod task_status_serde {
14 | use serde::{self, Deserialize, Deserializer, Serializer};
15 |
16 | pub fn serialize(status: &Option, s: S) -> Result
17 | where
18 | S: Serializer,
19 | {
20 | if let Some(ref d) = *status {
21 | let status = match d {
22 | taskchampion::Status::Pending => "pending",
23 | taskchampion::Status::Completed => "completed",
24 | taskchampion::Status::Deleted => "deleted",
25 | taskchampion::Status::Recurring => "recurring",
26 | taskchampion::Status::Unknown(v) => v.as_ref(),
27 | };
28 | return s.serialize_str(status);
29 | }
30 | s.serialize_none()
31 | }
32 |
33 | pub fn deserialize<'de, D>(deserializer: D) -> Result
, D::Error>
34 | where
35 | D: Deserializer<'de>,
36 | {
37 | let s: Option = Option::deserialize(deserializer)?;
38 | if let Some(s) = s {
39 | let t = s.to_lowercase();
40 | return Ok(Some(match t.as_str() {
41 | "pending" => taskchampion::Status::Pending,
42 | "completed" => taskchampion::Status::Completed,
43 | "deleted" => taskchampion::Status::Deleted,
44 | "recurring" => taskchampion::Status::Recurring,
45 | &_ => taskchampion::Status::Unknown(t),
46 | }));
47 | }
48 |
49 | Ok(None)
50 | }
51 | }
52 |
53 | pub(crate) mod task_date_format {
54 | use chrono::{DateTime, NaiveDateTime, Utc};
55 | use serde::{self, Deserialize, Deserializer, Serializer};
56 |
57 | const FORMAT: &'static str = "%Y%m%dT%H%M%SZ"; // Is always in UTC, not able to parse %:z
58 |
59 | // The signature of a serialize_with function must follow the pattern:
60 | //
61 | // fn serialize(&T, S) -> Result
62 | // where
63 | // S: Serializer
64 | //
65 | // although it may also be generic over the input types T.
66 | pub fn serialize(date: &Option>, serializer: S) -> Result
67 | where
68 | S: Serializer,
69 | {
70 | if let Some(dt) = date {
71 | let s = format!("{}", dt.format(FORMAT));
72 | serializer.serialize_str(&s)
73 | } else {
74 | serializer.serialize_none()
75 | }
76 | }
77 |
78 | // The signature of a deserialize_with function must follow the pattern:
79 | //
80 | // fn deserialize<'de, D>(D) -> Result
81 | // where
82 | // D: Deserializer<'de>
83 | //
84 | // although it may also be generic over the output types T.
85 | pub fn deserialize<'de, D>(deserializer: D) -> Result
>, D::Error>
86 | where
87 | D: Deserializer<'de>,
88 | {
89 | let s: Option = Option::deserialize(deserializer)?;
90 | if let Some(date_str) = s {
91 | match NaiveDateTime::parse_from_str(&date_str, FORMAT) {
92 | Ok(dt) => Ok(Some(DateTime::::from_naive_utc_and_offset(dt, Utc))),
93 | Err(_) => Ok(None),
94 | }
95 | } else {
96 | Ok(None)
97 | }
98 | }
99 | }
100 |
101 | pub(crate) mod task_date_format_mandatory {
102 | use chrono::{DateTime, NaiveDateTime, Utc};
103 | use serde::{self, Deserialize, Deserializer, Serializer};
104 |
105 | const FORMAT: &'static str = "%Y%m%dT%H%M%SZ"; // Is always in UTC, not able to parse %:z
106 |
107 | // The signature of a serialize_with function must follow the pattern:
108 | //
109 | // fn serialize(&T, S) -> Result
110 | // where
111 | // S: Serializer
112 | //
113 | // although it may also be generic over the input types T.
114 | pub fn serialize(date: &DateTime, serializer: S) -> Result
115 | where
116 | S: Serializer,
117 | {
118 | let s = format!("{}", date.format(FORMAT));
119 | serializer.serialize_str(&s)
120 | }
121 |
122 | // The signature of a deserialize_with function must follow the pattern:
123 | //
124 | // fn deserialize<'de, D>(D) -> Result
125 | // where
126 | // D: Deserializer<'de>
127 | //
128 | // although it may also be generic over the output types T.
129 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error>
130 | where
131 | D: Deserializer<'de>,
132 | {
133 | let date_str = String::deserialize(deserializer)?;
134 | let date_obj = NaiveDateTime::parse_from_str(&date_str, FORMAT)
135 | .map_err(serde::de::Error::custom)?;
136 | Ok(DateTime::::from_naive_utc_and_offset(date_obj, Utc))
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/frontend/templates/task_delete_confirm.html:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
Delete: {{ task.description }}
13 |
14 | Deleting a task will stop any operation on the task and will finally delete it. This operation can still be reverted.
15 | Are you sure you want to delete the task?
16 |
276 |
277 | {% for annotation in task.annotations %}
278 |
279 |
{{date(date=annotation.entry) }}
280 |
{{ annotation.description | linkify | safe }}
281 | {% if annotate_shortcuts %}
282 |
283 |
293 |
294 | {% endif %}
295 |
296 | {% endfor %}
297 |
298 |
299 |
300 |
301 | {% endif %}
302 |
303 |
304 |
310 |
311 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Current Update
4 |
5 | Hey!
6 | Small bug fixes, code cleanup, and support for auto-reload using `systemfd` during development. Not suggested for daily usage.
7 |
8 |
9 | [I would appreciate if you can support the development effort](https://tmahmood.gumroad.com/coffee)
10 |
11 |
12 | Please report any bugs, contributions are welcome.
13 |
14 | # What is this?
15 |
16 | A Minimalistic Web UI for Task Warrior focusing on Keyboard navigation.
17 |
18 | It's completely local. No intention to have any kind of online interactions.
19 | Font in the screenshot is [Maple Mono NF](https://github.com/subframe7536/maple-font)
20 |
21 | ## Stack
22 |
23 | - [Rust](https://www.rust-lang.org/) [nightly, will fail to build on stable]
24 | - [axum](https://github.com/tokio-rs/axum)
25 | - [tera](https://github.com/Keats/tera)
26 | - [TailwindCSS](https://tailwindcss.com/)
27 | - [daisyUI](https://daisyui.com/)
28 | - [HTMX](https://htmx.org)
29 | - [hotkeys](https://github.com/jaywcjlove/hotkeys-js)
30 | - [rollup](https://rollupjs.org/)
31 | - [Taskwarrior](https://taskwarrior.org/) (obviously :))
32 | - [Timewarrior](https://timewarrior.net)
33 |
34 | Still work in progress. But in the current stage it is pretty usable. You can see the list at the bottom, for what I intend to add, and what's been done.
35 |
36 | 
37 |
38 | # Using Release Binary
39 |
40 | Latest release binaries are now available. Check the release tags on the sidebar
41 |
42 | # Using Docker
43 |
44 | Docker image is provided. A lot of thanks go to [DCsunset](https://github.com/DCsunset/taskwarrior-webui)
45 | and [RustDesk](https://github.com/rustdesk/rustdesk/)
46 |
47 | ```shell
48 | docker build -t taskwarrior-web-rs . \
49 | && docker run --init -d -p 3000:3000 \
50 | -v ~/.task/:/app/taskdata/ \
51 | -v ~/.taskrc:/app/.taskrc \
52 | -v ~/.timewarrior/:/app/.timewarrior/ \
53 | --name taskwarrior-web-rs taskwarrior-web-rs
54 | ```
55 |
56 | As a service, every push to the `main` branch of this repository will provide automatic a docker image and can be pulled via
57 |
58 | ```shell
59 | docker pull ghcr.io/tmahmood/taskwarrior-web:main
60 | ```
61 |
62 | That should do it.
63 |
64 | ## Volumes
65 |
66 | The docker shares following directories as volumes to store data:
67 |
68 | | Volume path | Purpose |
69 | | ----------------- | ---------------------------------------------- |
70 | | /app/taskdata | Stores task data (mostly taskchampion.sqlite3) |
71 | | /app/.timewarrior | Stores timewarrior data |
72 | | /app/.config/taskwarrior-web | Stores taskwarrior-web configuration file |
73 |
74 | It is recommend to specify the corresponding volume in order to persist the data.
75 |
76 | ## Ports
77 |
78 | `taskwarrior-web` is by default internally listening on port `3000`:
79 |
80 | | Port | Protocol | Purpose |
81 | | ---- | -------- | -------------------------------- |
82 | | 3000 | tcp | Main webserver to serve the page |
83 |
84 | ## Environment variables
85 |
86 | In order to configure the environment variables and contexts for `timewarrior-web`, docker environments can be specified:
87 |
88 | | Docker environment | Shell environment | Purpose |
89 | | -------------------------------- | ----------------------- | -------------------------------------------------------- |
90 | | TASK_WEB_TWK_SERVER_PORT | TWK_SERVER_PORT | Specifies the server port (see "Ports") |
91 | | TASK_WEB_DISPLAY_TIME_OF_THE_DAY | DISPLAY_TIME_OF_THE_DAY | Displays a time of the day widget in case of value `1` |
92 | | TASK_WEB_TWK_USE_FONT | TWK_USE_FONT | Font to be used. If not, browsers default fonts are used |
93 | | TASK_WEB_TWK_THEME | TWK_THEME | Defines the theme to be used (see "Themes") |
94 |
95 | ## Hooks
96 |
97 | NOTE: If you have any hooks
98 | (eg. Starting time tracking using time-warrior when we start a task,
99 | you'll need to install the required application in in the docker, also the config files)
100 |
101 | By default, the `timewarrior` on-modify hook is installed.
102 |
103 | # Manual Installation
104 |
105 | ## Requirements
106 |
107 | - rust nightly
108 | - npm
109 |
110 | ### Installing rust nightly
111 |
112 | Should be installable through `rustup`
113 | https://rustup.rs/
114 |
115 | ### Building and Running
116 |
117 | 1. Clone the latest version from GitHub.
118 | 2. `cargo run --release`
119 |
120 | That should be it! Now you have the server running at `localhost:3000` accessible by your browser.
121 |
122 | ### Troubleshooting
123 |
124 | By default the log level is set to `INFO`. If a more detailed log is required, the application can be run with DEBUG or even TRACE messages.
125 | For debug messages, just set the environment "RUST_LOG" to "DEBUG":
126 | ```shell
127 | env RUST_LOG="DEBUG" cargo run
128 | ```
129 |
130 | If a fine granular configuration is desired - the application log itself is captured with the name `taskwarrior_web`.
131 |
132 | ## Customizing
133 |
134 | ### Customizing the port
135 |
136 | By default, the program will use 3000 as port,
137 | you can customize through `.env` file or enviornment variable, check `env.example`
138 |
139 | variable name: `TWK_SERVER_PORT`
140 |
141 | ### Displaying `time of the day` widget
142 |
143 | By default the "time of the day" widget is not visible, to display it put
144 |
145 | `DISPLAY_TIME_OF_THE_DAY=1`
146 |
147 | in the `.env` file
148 |
149 | ### Font customization
150 |
151 | Previously the app used `Departure Mono` as default font, which was also included in the repo.
152 | It's now removed.
153 | And the font can be set using env variable.
154 |
155 | Add the following to change default font:
156 |
157 | `TWK_USE_FONT='Maple Mono'`
158 |
159 | ### Themes
160 |
161 | By default, `taskwarrior-web` provides two themes:
162 |
163 | 1. taskwarrior-dark (intended for dark mode)
164 | 2. taskwarrior-light (intended for light mode)
165 |
166 | `taskwarrior-web` decides automatically based on the operating system and [browser preferences](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) whether the light or the dark theme should be shown.
167 |
168 | If a specific theme should be set fixed, the theme can be set as following in the environment:
169 |
170 | `TWK_THEME=taskwarrior-dark` (for dark-mode)
171 |
172 | # Using the app
173 |
174 | You can use Mouse or Keyboard to navigate.
175 |
176 | 
177 |
178 | - All the keyboard mnemonics are underlined.
179 | - The `Cmd Bar` needs to be focused (`Ctrl + Shift + K`) for the keyboard shortcuts to work
180 |
181 | ## Project and Tag selection
182 |
183 | Keyboard shortcut is `t`
184 |
185 | For selecting tag, once you enter tag selection mode, the `tag bar` is visible,
186 | tag mnemonics are displayed on the tags, in red boxes, typing the mnemonics will immediately set the tag/project,
187 |
188 | Note: selecting the tag/project again will remove the tag from filter.
189 |
190 | 
191 |
192 | ## Creating new task
193 |
194 | Keyboard shortcut is `n`
195 |
196 | Which should bring up the new task dialog box. It will use the current tags and project to create the task
197 | 
198 |
199 | ## Marking task as done or displaying task details
200 |
201 | Call up task search: `s`
202 | This should update top bar with the following, and also the task mnemonics are displayed with the id, in red boxes.
203 | Typing the mnemonics will immediately mark the task as done,
204 | or display the details of the task depending on mnemonics typed
205 |
206 | 
207 |
208 | In Task Details window, you can mark task as done[d] and start/stop [s] timer.
209 | Also, denotate task using [n]
210 | You can use task command to modify the task.
211 | You only need to enter the modifications.
212 |
213 | 
214 |
215 | Once you start a timer it will be highlighted on the list
216 | 
217 |
218 | ## Undo
219 |
220 | Keyboard shortcut is `u`
221 |
222 | This will bring up undo confirmation dialog
223 | 
224 |
225 | ## Custom queries
226 |
227 | Task organization is a pretty personal thing. And depending on the project or individual base, custom workflows and reportings are required.
228 | Create a configuration file under Linux in `$HOME/.config/taskwarrior-web/config.toml` or under Windows in `%APPDATA%\taskwarrior-web\config.toml` and add custom queries.
229 |
230 | A configuration file can look like:
231 |
232 | ```toml
233 | [custom_queries]
234 |
235 | [custom_queries.completed_last_week]
236 | query = "end.after:today-1wk and status:completed"
237 | description = "completed last 7days"
238 |
239 | [custom_queries.due_today]
240 | query = "due:today"
241 | description = "to be done today"
242 | fixed_key = "ni" # this will override randomly generated key
243 | ```
244 |
245 | Following options for each query definition is available:
246 | | property | mandatory | meaning |
247 | | ----------- | --------- | ---------------------------------------------------------------------- |
248 | | query | X | specifies the query to be executed on `taskwarrior`. |
249 | | description | X | description to be shown in the Web-UI for recognizing the right query. |
250 | | fixed_key | | Can be specified as two characters which will hardcode the shortcut. |
251 |
252 | The query can be selected via keyboard shortcuts or via click on the right buttons.
253 | In order to select custom queries with the keyboard, first type in `q` as key for queries.
254 | A list is shown with available custom queries:
255 | 
256 |
257 | On each custom query, either a pre-defined shortcut key is shown or an automatic and cached shortcut is shown.
258 | The right one is typed and automatically the custom query is set:
259 |
260 | 
261 |
262 | As soon as one of the other reports like `next`, `pending` or others are selected, the custom query is unset and `taskwarrior-web` standard reports are shown.
263 |
264 | Beside of a configuration file, it is possible to configure via environment variables as well:
265 | ```shell
266 | env TWK_custom_queries__one_query__fixed_key=ni TWK_custom_queries__one_query__query="end.after:today-1wk and status:completed" TWK_custom_queries__one_query__description="completed last 7days" cargo run
267 | ```
268 |
269 | The same way it is possible to configure the docker container accordingly.
270 |
271 | ## Switch theme
272 |
273 | It is possible to switch the theme, which is saved in local storage too.
274 |
275 | For this following three symbols are used (left of the command bar):
276 |
277 | | Symbol | Purpose |
278 | | ------ | ------------------------------------ |
279 | | ⚹ | Auto Mode or forced mode from server |
280 | | ☽ | Dark mode |
281 | | 🌣 | Light mode |
282 |
283 | # WIP warning
284 |
285 | This is a work in progress application, many things will not work,
286 | there will be errors, as no checks, and there may not be any error messages in case of error.
287 |
288 | # Past Updates
289 |
290 | Now the program is MIT licensed. Thanks to [monofox](https://github.com/monofox) for reminding me of it. And I appreciate his awesome contributions!
291 |
292 |
293 | Even though currently the program is not being updated, I have not given up on it. I will try to get some updates in eventually. Meantime, I may not be able to rectify any issues, but will do my best to give suggestions.
294 |
295 | I will be working on
296 | - UI update on Sync
297 | - Respecting context set in TW
298 |
299 | - Updated to tailwindcss 4 and using daisyui for UI components.
300 | - Cleaned-up code a bit to make it easier to manage
301 |
302 | ## Planned
303 |
304 | - [ ] Better configuration
305 | - [ ] Usability improvements on a long task list
306 | - [x] Hiding empty columns
307 | - [ ] Temporary highlight last modified row, if visible
308 | - [x] Make the mnemonics same for tags on refresh
309 | - [x] Modification
310 | - [x] Deleting
311 | - [ ] Following Context
312 | - [ ] Error handling
313 | - [x] Retaining input in case of error
314 | - [ ] Finetune error handling
315 | - [ ] Add more tests
316 | - [ ] Convert to desktop app using Tauri
317 | - [ ] Reporting
318 | - [ ] Project wise progress
319 | - [ ] Burndown reports
320 | - [ ] Column customization
321 | - [ ] Color customization
322 | - [ ] Time warrior integration, and time reporting
323 | - [ ] Searching by tag name
324 |
325 | ## Issues
326 |
327 | - [ ] Not able to select and copy tags, maybe add a copy button
328 | - [ ] Keyboard shortcut applied when there is a shortcut key and I use a mnemonic
329 | - [x] When marking task as done stop if active
330 |
331 | 
332 |
--------------------------------------------------------------------------------
/frontend/templates/tasks.html:
--------------------------------------------------------------------------------
1 | {% import "desc.html" as desc %}
2 |
11 |
12 | {% if has_toast %}
13 |
14 | {% include 'flash_msg.html' %}
15 |
16 | {% endif %}
17 |
18 |
19 |
20 |
21 |
22 | {% set on_all = "all" %}
23 | {% set on_complete = "btn-success" %}
24 | {% set on_pending = "btn-warning" %}
25 | {% set on_waiting = "btn-accent" %}
26 | {% set mod_key = "" %}
27 |
28 | {% include 'left_action_bar.html' %}
29 |
30 |
31 | {% for f in current_filter %}
32 |
44 | {% endfor %}
45 |
46 |