Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tsk

A keyboard-only timesheet client for the terminal. It lists the tasks assigned to you in Strativ's ERP360 (Odoo 16), expands each one into its timesheet lines, and logs, edits and deletes hours without leaving the keyboard.

tsk filtering 22 tasks down to three by typing "discuss" in the search field

Written in Go with Bubble Tea. One static binary, no CGO.

Contents

Features

  • All your tasks in one list. Pulled from ERP360, no hunting through the web UI.
  • See the hours already logged. Open a task, its entries show up as a table.
  • Log time in a few keystrokes. Date, what you did, hours — saved straight into ERP360.
  • Fix mistakes. Change or delete any entry you logged, from the same screen.
  • Typed hours are never lost. A failed save keeps them on screen and says so.
  • Only your own hours. A colleague's time on the same task never counts as yours.
  • "Where did Tuesday go?" One key lists a whole day across every task, with its total.
  • A progress bar for today. How far through your 8 hours, updated as you type.
  • Find a task by typing. The list narrows as you go.
  • No mouse, ever. The keys you can press are always listed along the bottom.
  • Change any key. Rebind anything you like in a small text file.
  • Your API key stays private. Encrypted at rest, never written to a file or a screen.
  • Works with no internet. Everything is cached; entries reach the ERP when you are back.

Installation

Go 1.22 or newer is the only hard requirement. Linux and macOS are the same commands; there is no CGO and nothing platform-specific in the build.

git clone git@github.com:tasnim-strativ/tsk-tui.git
cd tsk-tui
go install ./cmd/tsk        # ~/go/bin/tsk

Or build in place:

go build -o tsk ./cmd/tsk
./tsk

Make sure ~/go/bin is on your PATH if you used go install.

pass is what keeps your API key encrypted, and it is the recommended way to run this — see Option A. On macOS: brew install pass gnupg, plus brew install pinentry-mac so the GPG passphrase prompt has somewhere to appear. On Linux, your package manager has pass.

If you would rather not run a password store, Option B works without it.

First run

You need an Odoo API key and the database name. Generate the key in ERP360 under Preferences → Account Security → New API Key.

The database name is deliberately not in this repo — it is half of what someone needs to reach the ERP over JSON-RPC. Two ways to get it:

  • Ask odoo baba on Slack.

  • Read it from your own session: log in to erp360.strativ.se, open the browser console and run

    await fetch('/web/session/get_session_info', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: '{"jsonrpc":"2.0","method":"call","params":{}}'
    }).then(r => r.json()).then(d => d.result.db)

    The string it prints is the database name.

Now hand both to tsk, either way below. Pick one — each is complete on its own.

Option A — a pass entry (recommended)

Uses pass, the standard Unix password manager, so the key stays encrypted at rest and tsk can rotate it for you with K. Both values live in one entry, secret on the first line and the database below it, which is pass's own convention for metadata:

pass insert -m tsk/api-key

Type the two lines and end with ctrl+d. The entry should look like this — the API key first, on its own line, and the database on a db: line under it:

9f3c1a7e2b5d8406f1ae7c3b95d20e4867fa1cb2
db: example_erp_db

That is exactly what pass show tsk/api-key prints back. tsk reads line one as the key and looks for db: in the lines below it, so the order matters — a db: line on top would be read as your key.

(Both values above are made up — use your own key and the database name you looked up.)

Then run tsk. If the entry does not exist yet, it asks for the key on the first screen, saves it to pass, and fetches. esc at that prompt works offline on whatever is cached.

Option B — environment variables (no pass)

Nothing to install, nothing encrypted. tsk reads $TSK_API_KEY before it looks for a password store, so pass is never invoked at all.

Put the two values in your shell's config file, so they are there in every shell and you never type them again. Open the file for your shell in an editor and add:

fish~/.config/fish/config.fish

set -gx TSK_API_KEY 9f3c1a7e2b5d8406f1ae7c3b95d20e4867fa1cb2
set -gx TSK_ODOO_DB example_erp_db

zsh~/.zshrc · bash~/.bashrc

export TSK_API_KEY=9f3c1a7e2b5d8406f1ae7c3b95d20e4867fa1cb2
export TSK_ODOO_DB=example_erp_db

Both values above are made up — use your own key and the database name you looked up.

Then pick up the change and run it:

exec fish            # or: source ~/.zshrc — or just open a new terminal
tsk

Three things to know about this route, and why Option A is the recommended one:

  • Edit the file, do not type the export at your prompt. A command line with the key in it lands in your shell history in plain text.
  • The file is plaintext. Keep it to yourself — chmod 600 ~/.config/fish/config.fish (or ~/.zshrc) — and remember the key is then in the environment of every process that shell starts.
  • K cannot rotate the key on this route, because saving goes through pass insert. Change the key by editing the same config file.

Either way

Without a database name — no db: line and no $TSK_ODOO_DB — the task list still loads, but timesheet lines cannot: the status line will say no Odoo database.

Usage

tsk starts on the task list. Press l to open a task, a to add an entry, enter to edit the row under the cursor, d to delete it, q to quit.

Every mode's keys are in the footer, always. The footer is generated from the same bindings the app matches on, so it cannot drift from what the keys actually do.

Keymap

These are the defaults — see Custom keybindings to change them.

Task list (where it starts)

Key Does
j / k next / previous task
g / G first / last
ctrl+f / ctrl+b half a screen down / up
l expand the task, focus its rows
h collapse
/ date jump — lists that day across every task
r re-fetch tasks from the ERP
K replace the stored API key
i focus the search field
ctrl+u clear the search and focus it
q quit (asks first; ctrl+c quits at once)

Inside a task

Key Does
j / k, g / G, ctrl+f / ctrl+b move between rows
a new entry at the top, dated today
enter edit the focused row in place
d delete the focused row (asks; y only)
/ jump to a date within this task
h collapse and go back to the task line
esc back to the task line, still expanded

Editing an entry

Key Does
tab / shift+tab date → description → hours → ✓ → ✕
enter next field, or commit on ✓
ctrl+u clear the current field
esc cancel

Search field

Key Does
any key filter by title and tag, live
esc / enter back to the task list
ctrl+u clear the query and collapse everything

Destructive prompts — quitting and deleting a line — take y only, so a reflexive enter cannot fire them.

Typing hours and dates

Durations are read however you write them, and normalized when you leave the field:

7h30m → 7:30      7.5 → 7:30      90m → 1:30      7:30 → 7:30      7 → 7:00

Dates fill in what you leave out, relative to the row you are editing (or today for a new entry):

8       → 08/08/26     (day only: keeps the month and year)
8/9     → 08/09/26     (day and month: keeps the year)
8/9/26  → 08/09/26

When you edit a row the date arrives selected — the first keystroke replaces it, tab keeps it.

The date jump

/ means two different things, because there are two different questions:

  • From the task list — "where did that day go?" /12 resolves to the 12th of this month and opens a modal listing every entry logged on it, in any task, with the day's total. Nothing expands. esc closes it.
  • Inside a task — "where is that row?" Here the date is matched part by part, so /12 finds the 12th of any month among that task's rows, /12/7 any 12th of July. The cursor moves to it; no modal covers the rows you are reading.

Either way the matching rows stay marked until you clear the jump (enter on an empty prompt, or ctrl+u).

Configuration

Custom keybindings

Every binding is a default, not a fact. Write the current keymap to the config file and edit what you like:

# Linux
mkdir -p ~/.config/tsk
tsk --print-keys > ~/.config/tsk/config.toml
# macOS
mkdir -p ~/Library/Application\ Support/tsk
tsk --print-keys > ~/Library/Application\ Support/tsk/config.toml
[keys]
half_down = ["ctrl+d"]          # back to vim's key
down      = ["j", "down", "n"]  # several keys per action
quit      = []                  # an empty list unbinds it; ctrl+c always quits

Keep only the lines you want to change — anything absent keeps its default, and no config file at all is perfectly fine. Action names are listed by --print-keys. Keys are spelled the way Bubble Tea spells them: a single character, a ctrl+ / alt+ / shift+ compound, or one of enter esc tab shift+tab space backspace delete up down left right home end pgup pgdown.

A misspelled key or an unknown action stops the program with a message rather than starting up with a keymap that silently does nothing.

The footer follows a rebind on its own, since help and behavior read the same bindings.

Environment variables

Variable Default Purpose
TSK_API_KEY use this key and skip pass entirely
TSK_PASS_NAME tsk/api-key which pass entry holds the key
TSK_ODOO_DB Odoo database, overriding the db: line in pass
TSK_API_URL https://erp360.strativ.se point at staging or a local Odoo
XDG_CONFIG_HOME ~/.config on Linux, ~/Library/Application Support on macOS where the config and cache live

How syncing works

The REST API and JSON-RPC each cover part of the job:

Call Used for
GET /api/v1/tasks/my your task list
GET /api/v1/timesheets/hour-log-summary today's total, for the progress bar
POST /api/v1/timesheets/log creating one entry
JSON-RPC account.analytic.line search_read reading a task's lines
JSON-RPC write / unlink editing and deleting a line

Worth knowing:

  • Lines are read lazily. Expanding a task reads it once; r clears that so open tasks are read again.
  • A read merges, it never replaces. Entries you typed but the ERP has not confirmed are kept, and app-created rows carry negative ids so they cannot collide with an Odoo line.
  • Reads are filtered to you. Odoo's own task form lists every employee's lines; this does not.
  • Nothing retries by itself. One entry per request, and a timed-out retry could double-log, so a failure is reported and left for you to decide.
  • The API key lives in memory, the Authorization header, and nowhere else. Not in a URL, not in a log line, not in an error message, not in the cache file.

Files it touches

Its own directory, plus the pass entry if you chose Option A:

Platform Directory
Linux ~/.config/tsk/
macOS ~/Library/Application Support/tsk/

$XDG_CONFIG_HOME overrides that on either platform, so set -x XDG_CONFIG_HOME ~/.config on a Mac puts everything in ~/.config/tsk/ instead.

File What
tasks.json cached tasks and hours, so it works offline
config.toml your keybindings (optional)
pass entry tsk/api-key API key on line one, db: below it (Option A only)

tasks.json holds real hours and task names — worth keeping out of any repo or paste.

Troubleshooting

no Odoo database — add a db: line to the pass entry Timesheet lines come from JSON-RPC, which needs the database name. pass edit tsk/api-key and add db: <database-name> as the second line, or export TSK_ODOO_DB.

pass not found — install password-store, or export TSK_API_KEY You are on the pass route without pass. Either install it — brew install pass gnupg on macOS, your package manager on Linux — and initialise a store (pass init <gpg-id>), or switch to Option B and set TSK_API_KEY.

A GPG passphrase prompt appears Expected — pass show runs with the terminal handed over to it, so a tty pinentry works without corrupting the screen. gpg-agent caches it after the first time.

offline — showing the tasks on disk The ERP could not be reached. Everything still works against the cache, and hours you type are kept locally until a write succeeds.

A task shows no entries even though Odoo has some The first read failed (usually the missing database name). Collapse and expand it to try again; failures are not remembered as "already read".

401 / the key prompt opens by itself The key was rejected or revoked. K stores a new one.

ctrl+f or ctrl+u seems to be swallowed Your terminal multiplexer or shell may claim it first. ctrl+l was dropped as a binding for exactly this reason. Rebind anything that clashes — see Custom keybindings.

Development

cmd/tsk/         entry point
internal/model/  root model, one Update handler per mode, View
internal/parse/  hours and dates — pure, table-tested
internal/store/  the cache on disk, and the key from pass
internal/api/    REST client and Odoo JSON-RPC
internal/config/ the config.toml keymap, and where it lives per platform
internal/theme/  every color and style, in one place
go test ./... && go vet ./...      # the gate before any change is done

Conventions worth keeping: no I/O in Update or View (disk and network go through a tea.Cmd), minutes are stored as minutes and formatted only when rendered, totals and the progress bar are derived on every render rather than cached, and every parsing rule has a table-driven test. The rendering tests assert real terminal widths — a line wider than the screen wraps and pushes the layout out of place. CLAUDE.md carries the longer reasoning behind these decisions.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages