feat(segments): add todoist integration

This commit is contained in:
kostadin-tonchekliev
2026-01-22 21:33:34 +01:00
committed by Jan De Dobbeleer
parent 05c7b6f383
commit 6100c483cd
6 changed files with 301 additions and 0 deletions
+4
View File
@@ -130,6 +130,7 @@ func init() {
gob.Register(&segments.Terraform{})
gob.Register(&segments.Text{})
gob.Register(&segments.Time{})
gob.Register(&segments.Todoist{})
gob.Register(&segments.UI5Tooling{})
gob.Register(&segments.Umbraco{})
gob.Register(&segments.Unity{})
@@ -342,6 +343,8 @@ const (
TEXT SegmentType = "text"
// TIME writes the current timestamp
TIME SegmentType = "time"
// TODOIST segment
TODOIST SegmentType = "todoist"
// UI5 Tooling segment
UI5TOOLING SegmentType = "ui5tooling"
// UMBRACO writes the Umbraco version if Umbraco is present
@@ -467,6 +470,7 @@ var Segments = map[SegmentType]func() SegmentWriter{
TERRAFORM: func() SegmentWriter { return &segments.Terraform{} },
TEXT: func() SegmentWriter { return &segments.Text{} },
TIME: func() SegmentWriter { return &segments.Time{} },
TODOIST: func() SegmentWriter { return &segments.Todoist{} },
UI5TOOLING: func() SegmentWriter { return &segments.UI5Tooling{} },
UMBRACO: func() SegmentWriter { return &segments.Umbraco{} },
UNITY: func() SegmentWriter { return &segments.Unity{} },
+61
View File
@@ -0,0 +1,61 @@
package segments
import (
"encoding/json"
"net/http"
"github.com/jandedobbeleer/oh-my-posh/src/log"
"github.com/jandedobbeleer/oh-my-posh/src/segments/options"
)
type Todoist struct {
Base
TaskCount int
}
type TasksResponse struct {
Results []Task `json:"results"`
}
type Task struct {
ID string `json:"id"`
}
func (t *Todoist) Enabled() bool {
err := t.GetData()
if err != nil {
log.Error(err)
return false
}
return true
}
func (t *Todoist) Template() string {
return "{{ .TaskCount }} "
}
func (t *Todoist) GetData() error {
apikey := t.options.Template(APIKey, ".", t)
httpTimeout := t.options.Int(options.HTTPTimeout, options.DefaultHTTPTimeout)
addHeaders := func(req *http.Request) {
req.Header.Set("Authorization", "Bearer "+apikey)
req.Header.Set("Accept", "application/json")
}
body, err := t.env.HTTPRequest("https://api.todoist.com/api/v1/tasks/filter?query=due today", nil, httpTimeout, addHeaders)
if err != nil {
return err
}
var response TasksResponse
if err := json.Unmarshal(body, &response); err != nil {
return err
}
t.TaskCount = len(response.Results)
return nil
}
+147
View File
@@ -0,0 +1,147 @@
package segments
import (
"errors"
"testing"
"github.com/jandedobbeleer/oh-my-posh/src/runtime/mock"
"github.com/jandedobbeleer/oh-my-posh/src/segments/options"
"github.com/stretchr/testify/assert"
)
const TodoistTestURL = "https://api.todoist.com/api/v1/tasks/filter?query=due today"
func TestTodoistSegment(t *testing.T) {
cases := []struct {
Error error
Case string
JSONResponse string
ExpectedCount int
ExpectedEnabled bool
}{
{
Case: "No tasks",
JSONResponse: `{"results": []}`,
ExpectedCount: 0,
ExpectedEnabled: true,
},
{
Case: "Single task",
JSONResponse: `{"results": [{"id": "123"}]}`,
ExpectedCount: 1,
ExpectedEnabled: true,
},
{
Case: "Multiple tasks",
JSONResponse: `{"results": [{"id": "1"}, {"id": "2"}, {"id": "3"}]}`,
ExpectedCount: 3,
ExpectedEnabled: true,
},
{
Case: "API error",
JSONResponse: ``,
ExpectedCount: 0,
ExpectedEnabled: false,
Error: errors.New("API request failed"),
},
{
Case: "Invalid JSON response",
JSONResponse: `invalid json`,
ExpectedCount: 0,
ExpectedEnabled: false,
},
{
Case: "Task with additional fields",
JSONResponse: `{"results": [{"id": "456", "content": "Buy milk", "due": {"date": "2024-01-15"}}]}`,
ExpectedCount: 1,
ExpectedEnabled: true,
},
}
for _, tc := range cases {
t.Run(tc.Case, func(t *testing.T) {
env := new(mock.Environment)
props := options.Map{
APIKey: "fake-api-key",
}
env.On("HTTPRequest", TodoistTestURL).Return([]byte(tc.JSONResponse), tc.Error)
todoist := &Todoist{}
todoist.Init(props, env)
enabled := todoist.Enabled()
assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case)
if enabled {
assert.Equal(t, tc.ExpectedCount, todoist.TaskCount, tc.Case)
}
})
}
}
func TestTodoistTemplate(t *testing.T) {
todoist := &Todoist{}
assert.Equal(t, "{{ .TaskCount }} ", todoist.Template())
}
func TestTodoistTemplateRendering(t *testing.T) {
cases := []struct {
Case string
JSONResponse string
Template string
ExpectedString string
}{
{
Case: "Default template with no tasks",
JSONResponse: `{"results": []}`,
Template: "{{ .TaskCount }}",
ExpectedString: "0",
},
{
Case: "Default template with tasks",
JSONResponse: `{"results": [{"id": "1"}, {"id": "2"}]}`,
Template: "{{ .TaskCount }}",
ExpectedString: "2",
},
{
Case: "Custom template with icon",
JSONResponse: `{"results": [{"id": "1"}, {"id": "2"}, {"id": "3"}]}`,
Template: "📋 {{ .TaskCount }} tasks",
ExpectedString: "📋 3 tasks",
},
{
Case: "Conditional template - has tasks",
JSONResponse: `{"results": [{"id": "1"}]}`,
Template: "{{ if gt .TaskCount 0 }}📋 {{ .TaskCount }}{{ end }}",
ExpectedString: "📋 1",
},
{
Case: "Conditional template - no tasks",
JSONResponse: `{"results": []}`,
Template: "{{ if gt .TaskCount 0 }}📋 {{ .TaskCount }}{{ else }}✅{{ end }}",
ExpectedString: "✅",
},
}
for _, tc := range cases {
t.Run(tc.Case, func(t *testing.T) {
env := new(mock.Environment)
props := options.Map{
APIKey: "fake-api-key",
}
env.On("HTTPRequest", TodoistTestURL).Return([]byte(tc.JSONResponse), nil)
todoist := &Todoist{}
todoist.Init(props, env)
enabled := todoist.Enabled()
assert.True(t, enabled, tc.Case)
result := renderTemplate(env, tc.Template, todoist)
assert.Equal(t, tc.ExpectedString, result, tc.Case)
})
}
}
+29
View File
@@ -457,6 +457,7 @@
"terraform",
"text",
"time",
"todoist",
"ui5tooling",
"umbraco",
"unity",
@@ -4459,6 +4460,34 @@
}
}
},
{
"if": {
"properties": {
"type":{
"const": "todoist"
}
}
},
"then": {
"title": "Todoist Segment",
"description": "https://ohmyposh.dev/docs/segments/system/todoist",
"properties": {
"options": {
"properties": {
"api_key": {
"type": "string",
"title": "API Key",
"description": "API Key (Required)",
"default": "."
},
"http_timeout": {
"$ref": "#/definitions/http_timeout"
}
}
}
}
}
},
{
"if": {
"properties": {
+59
View File
@@ -0,0 +1,59 @@
---
id: todoist
title: Todoist
sidebar_label: Todoist
---
## What
Displays your daily tasks from [Todoist][todoist].
:::caution
The segment needs an [API Key][guide] from your Todoist profile for this to work.
:::
## Sample Configuration
import Config from "@site/src/components/Config.js";
<Config
data={{
type: "todoist",
style: "powerline",
powerline_symbol: "\uE0B0",
foreground: "#ffffff",
background: "#FF0000",
template: "{{.TaskCount}}",
options: {
api_key: "<YOUR_API_KEY>",
http_timeout: 500,
},
}}
/>
## Options
| Name | Type | Default | Description |
| -------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `api_key` | `string` | `.` | Your API Key from [Todoist][todoist] |
| `http_timeout` | `int` | `20` | The time (_in milliseconds_, `ms`) it takes to consider an http request as **timed-out**. If no segment is shown, try increasing this timeout. |
## Template ([info][templates])
:::note default template
```template
{{ .TaskCount }}
```
:::
### Properties
| Name | Type | Description |
| ------------ | ----- | ----------------------------- |
| `.TaskCount` | `int` | the number of tasks due today |
[todoist]: https://www.todoist.com/
[templates]: /docs/configuration/templates
[guide]: https://www.todoist.com/help/articles/find-your-api-token-Jpzx9IIlB
+1
View File
@@ -206,6 +206,7 @@ export default {
"segments/web/ipify",
"segments/web/nba",
"segments/web/owm",
"segments/web/todoist",
"segments/web/wakatime",
]
},