feat(segments): add taskwarrior segment

This commit is contained in:
Matthias Eck
2026-03-15 10:32:49 +01:00
committed by Jan De Dobbeleer
parent 6e19a8f514
commit ea0dc668e5
6 changed files with 392 additions and 0 deletions
+4
View File
@@ -127,6 +127,7 @@ func init() {
gob.Register(&segments.Swift{})
gob.Register(&segments.SystemInfo{})
gob.Register(&segments.TalosCTL{})
gob.Register(&segments.Taskwarrior{})
gob.Register(&segments.Tauri{})
gob.Register(&segments.Terraform{})
gob.Register(&segments.Text{})
@@ -338,6 +339,8 @@ const (
SYSTEMINFO SegmentType = "sysinfo"
// TALOSCTL writes the talosctl context
TALOSCTL SegmentType = "talosctl"
// TASKWARRIOR writes Taskwarrior task counts and context
TASKWARRIOR SegmentType = "taskwarrior"
// Tauri Segment
TAURI SegmentType = "tauri"
// TERRAFORM writes the terraform workspace we're currently in
@@ -470,6 +473,7 @@ var Segments = map[SegmentType]func() SegmentWriter{
SWIFT: func() SegmentWriter { return &segments.Swift{} },
SYSTEMINFO: func() SegmentWriter { return &segments.SystemInfo{} },
TALOSCTL: func() SegmentWriter { return &segments.TalosCTL{} },
TASKWARRIOR: func() SegmentWriter { return &segments.Taskwarrior{} },
TAURI: func() SegmentWriter { return &segments.Tauri{} },
TERRAFORM: func() SegmentWriter { return &segments.Terraform{} },
TEXT: func() SegmentWriter { return &segments.Text{} },
+69
View File
@@ -0,0 +1,69 @@
package segments
import (
"strings"
c "golang.org/x/text/cases"
"golang.org/x/text/language"
"github.com/jandedobbeleer/oh-my-posh/src/log"
"github.com/jandedobbeleer/oh-my-posh/src/segments/options"
)
// Taskwarrior option constants
const (
TaskwarriorCommand options.Option = "command"
TaskwarriorCommands options.Option = "commands"
)
// Taskwarrior displays task counts and context from Taskwarrior.
// The Commands field is a map from capitalized command name to the raw output
// of the corresponding Taskwarrior invocation. Each entry in the config map
// has the command name as key and a full Taskwarrior argument string as value.
type Taskwarrior struct {
Base
// Commands holds the raw output of each configured command, keyed by name
// with the first letter uppercased.
Commands map[string]string
}
func (t *Taskwarrior) Template() string {
return " \uf4a0 {{ range $k, $v := .Commands }}{{ $k }}:{{ $v }} {{ end }}"
}
func (t *Taskwarrior) Enabled() bool {
cmd := t.options.String(TaskwarriorCommand, "task")
if !t.env.HasCommand(cmd) {
return false
}
defaultCommands := map[string]string{
"due": "+PENDING due.before:tomorrow count",
"scheduled": "+PENDING scheduled.before:tomorrow count",
"waiting": "+WAITING count",
"context": "_get rc.context",
}
configuredCommands := t.options.KeyValueMap(TaskwarriorCommands, defaultCommands)
t.Commands = make(map[string]string, len(configuredCommands))
for name, args := range configuredCommands {
key := c.Title(language.English).String(name)
t.Commands[key] = t.runCommand(cmd, args)
}
return true
}
func (t *Taskwarrior) runCommand(cmd, args string) string {
output, err := t.env.RunCommand(cmd, strings.Fields(args)...)
if err != nil {
log.Error(err)
return ""
}
return strings.TrimSpace(output)
}
+184
View File
@@ -0,0 +1,184 @@
package segments
import (
"errors"
"strings"
"testing"
"github.com/jandedobbeleer/oh-my-posh/src/runtime/mock"
"github.com/jandedobbeleer/oh-my-posh/src/segments/options"
"github.com/stretchr/testify/assert"
)
func TestTaskwarrior(t *testing.T) {
cases := []struct {
ConfiguredCommands map[string]string
CommandOutputs map[string]string
CommandErrors map[string]error
ExpectedCommands map[string]string
Case string
Command string
HasCommand bool
ExpectedEnabled bool
}{
{
Case: "happy path default commands",
HasCommand: true,
CommandOutputs: map[string]string{
"+PENDING due.before:tomorrow count": "3",
"+PENDING scheduled.before:tomorrow count": "1",
"+WAITING count": "2",
},
ExpectedEnabled: true,
ExpectedCommands: map[string]string{
"Due": "3",
"Scheduled": "1",
"Waiting": "2",
"Context": "",
},
},
{
Case: "no command",
HasCommand: false,
ExpectedEnabled: false,
},
{
Case: "all zeros",
HasCommand: true,
CommandOutputs: map[string]string{
"+PENDING due.before:tomorrow count": "0",
"+PENDING scheduled.before:tomorrow count": "0",
"+WAITING count": "0",
},
ExpectedEnabled: true,
ExpectedCommands: map[string]string{
"Due": "0",
"Scheduled": "0",
"Waiting": "0",
"Context": "",
},
},
{
Case: "custom commands only",
HasCommand: true,
ConfiguredCommands: map[string]string{
"urgent": "+PENDING +OVERDUE count",
},
CommandOutputs: map[string]string{
"+PENDING +OVERDUE count": "5",
},
ExpectedEnabled: true,
ExpectedCommands: map[string]string{
"Urgent": "5",
},
},
{
Case: "context command via commands map",
HasCommand: true,
ConfiguredCommands: map[string]string{
"due": "+PENDING due.before:tomorrow count",
"context": "_get rc.context",
},
CommandOutputs: map[string]string{
"+PENDING due.before:tomorrow count": "3",
"_get rc.context": "work",
},
ExpectedEnabled: true,
ExpectedCommands: map[string]string{
"Due": "3",
"Context": "work",
},
},
{
Case: "command error returns empty string for that command",
HasCommand: true,
ConfiguredCommands: map[string]string{
"due": "+PENDING due.before:tomorrow count",
},
CommandErrors: map[string]error{
"+PENDING due.before:tomorrow count": errors.New("command failed"),
},
ExpectedEnabled: true,
ExpectedCommands: map[string]string{
"Due": "",
},
},
{
Case: "custom executable is used when TaskwarriorCommand is set",
Command: "task2",
ConfiguredCommands: map[string]string{
"due": "+PENDING due.before:tomorrow count",
},
CommandOutputs: map[string]string{
"+PENDING due.before:tomorrow count": "7",
},
HasCommand: true,
ExpectedEnabled: true,
ExpectedCommands: map[string]string{
"Due": "7",
},
},
}
for _, tc := range cases {
t.Run(tc.Case, func(t *testing.T) {
env := new(mock.Environment)
cmd := tc.Command
if cmd == "" {
cmd = "task"
}
env.On("HasCommand", cmd).Return(tc.HasCommand)
configuredCommands := tc.ConfiguredCommands
if configuredCommands == nil && tc.HasCommand {
// default commands
configuredCommands = map[string]string{
"due": "+PENDING due.before:tomorrow count",
"scheduled": "+PENDING scheduled.before:tomorrow count",
"waiting": "+WAITING count",
"context": "_get rc.context",
}
}
for _, args := range configuredCommands {
splitArgs := splitTaskArgs(args)
var output string
var err error
if tc.CommandOutputs != nil {
output = tc.CommandOutputs[args]
}
if tc.CommandErrors != nil {
err = tc.CommandErrors[args]
}
env.On("RunCommand", cmd, splitArgs).Return(output, err)
}
props := options.Map{}
if tc.ConfiguredCommands != nil {
props[TaskwarriorCommands] = tc.ConfiguredCommands
}
if tc.Command != "" {
props[TaskwarriorCommand] = tc.Command
}
tw := &Taskwarrior{}
tw.Init(props, env)
assert.Equal(t, tc.ExpectedEnabled, tw.Enabled(), tc.Case)
if !tc.ExpectedEnabled {
return
}
assert.Equal(t, tc.ExpectedCommands, tw.Commands, tc.Case)
})
}
}
// splitTaskArgs splits a space-separated argument string into a slice.
func splitTaskArgs(s string) []string {
return strings.Fields(s)
}
+41
View File
@@ -454,6 +454,7 @@
"swift",
"sysinfo",
"talosctl",
"taskwarrior",
"tauri",
"terraform",
"text",
@@ -4476,6 +4477,46 @@
}
}
},
{
"if": {
"properties": {
"type": {
"const": "taskwarrior"
}
}
},
"then": {
"title": "Taskwarrior Segment",
"description": "https://ohmyposh.dev/docs/segments/cli/taskwarrior",
"properties": {
"options": {
"properties": {
"command": {
"type": "string",
"title": "Taskwarrior Command",
"description": "The taskwarrior command to use",
"default": "task"
},
"commands": {
"type": "object",
"title": "Commands",
"description": "Map of name to Taskwarrior arguments; the trimmed stdout of each invocation is exposed as .Commands.<Name> in the template",
"additionalProperties": {
"type": "string"
},
"default": {
"due": "+PENDING due.before:tomorrow count",
"scheduled": "+PENDING scheduled.before:tomorrow count",
"waiting": "+WAITING count",
"context": "_get rc.context"
}
}
},
"unevaluatedProperties": false
}
}
}
},
{
"if": {
"properties": {
+93
View File
@@ -0,0 +1,93 @@
---
id: taskwarrior
title: Taskwarrior
sidebar_label: Taskwarrior
---
## What
Display [Taskwarrior][taskwarrior] task data for configurable commands. Each named command runs
`task` with the specified arguments and exposes the raw output in the template.
## Sample Configuration
import Config from "@site/src/components/Config.js";
<Config
data={{
type: "taskwarrior",
style: "powerline",
powerline_symbol: "\uE0B0",
foreground: "#193549",
background: "#ffeb3b",
template: " \uf4a0 {{ range $k, $v := .Commands }}{{ $k }}:{{ $v }} {{ end }}",
options: {
commands: {
due: "+PENDING due.before:tomorrow count",
scheduled: "+PENDING scheduled.before:tomorrow count",
waiting: "+WAITING count",
context: "_get rc.context",
},
},
}}
/>
## Options
| Name | Type | Default | Description |
| ---------- | :-----------------: | :-------: | ------------------------------------------------------------------------------ |
| `command` | `string` | `task` | the Taskwarrior executable to use |
| `commands` | `map[string]string` | see below | map of name to Taskwarrior arguments; the raw output is exposed in `.Commands` |
### Default `commands` value
```json
{
"due": "+PENDING due.before:tomorrow count",
"scheduled": "+PENDING scheduled.before:tomorrow count",
"waiting": "+WAITING count",
"context": "_get rc.context"
}
```
Each entry runs `task <arguments>` and stores the trimmed stdout as a string. Remove entries
you do not need to keep prompt rendering fast.
## Template ([info][templates])
:::note default template
```template
{{ "\uf4a0" }} {{ range $k, $v := .Commands }}{{ $k }}:{{ $v }} {{ end }}
```
:::
### Properties
| Name | Type | Description |
| ------------ | ------------------- | ------------------------------------------------------------------------------------ |
| `.Commands` | `map[string]string` | raw command output keyed by name with the first letter uppercased (e.g. `"Due"`) |
### Examples
Access a specific command result directly:
```template
{{ "\uf4a0" }} Due: {{ .Commands.Due }}
```
Display multiple results:
```template
{{ "\uf4a0" }} Due: {{ .Commands.Due }} | Waiting: {{ .Commands.Waiting }}
```
Include the active context alongside task counts:
```template
{{ "\uf4a0" }} {{ .Commands.Context }} - Due: {{ .Commands.Due }} Scheduled: {{ .Commands.Scheduled }}
```
[templates]: /docs/configuration/templates
[taskwarrior]: https://taskwarrior.org
+1
View File
@@ -79,6 +79,7 @@ export default {
"segments/cli/react",
"segments/cli/svelte",
"segments/cli/talosctl",
"segments/cli/taskwarrior",
"segments/cli/tauri",
"segments/cli/terraform",
"segments/cli/ui5tooling",