fuso 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +242 -0
- data/assets/screenshot_idle.png +0 -0
- data/assets/screenshot_report.png +0 -0
- data/assets/screenshot_tracking.png +0 -0
- data/exe/fuso +90 -0
- data/lib/fuso/app.rb +470 -0
- data/lib/fuso/config.rb +117 -0
- data/lib/fuso/heartbeat.rb +59 -0
- data/lib/fuso/session.rb +188 -0
- data/lib/fuso/styles.rb +181 -0
- data/lib/fuso/version.rb +5 -0
- data/lib/fuso/views/main_view.rb +115 -0
- data/lib/fuso/views/report_view.rb +239 -0
- data/lib/fuso/views/settings_view.rb +107 -0
- data/lib/fuso.rb +4 -0
- metadata +117 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: '079729abca60749f7ed805ad7acad6127d8858781e0a520182e1e3db70d80488'
|
|
4
|
+
data.tar.gz: 5af2c50fe031ea9e551a5d97cb9a2ce3cb6a3e94028306ff896e5bd0aa667cba
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 97151b014221fd818cebb75c4c4e0796a5b955d3e08e4ca8ff7227185c165673fc9775d429d441fd5afceba8ced8c133496b3cf902c0f874caab8964e7acc081
|
|
7
|
+
data.tar.gz: 1b2cf8b7a3ca6f212c681150e41dce076d15a9f7f41db4b6aa209c36960106d53ba24ea845d0946557aaa244c52aacd7df4ee2fa9b5885bf6876e40b53bf8bde
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Afonso
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<h1 align="center">Fuso</h1>
|
|
2
|
+
<p align="center">
|
|
3
|
+
<em>A dead-simple terminal time tracker.<br>One key to start. One key to switch. One key to stop.</em>
|
|
4
|
+
</p>
|
|
5
|
+
|
|
6
|
+
<p align="center">
|
|
7
|
+
<a href="#installation">Install</a> ·
|
|
8
|
+
<a href="#usage">Usage</a> ·
|
|
9
|
+
<a href="#reports">Reports</a> ·
|
|
10
|
+
<a href="#contributing">Contributing</a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
Most time tracking tools are either clunky web apps that demand your attention, or over-engineered CLIs with a dozen subcommands and flags. You just want to press **one key** when you sit down, and another key when you switch tasks. That's it.
|
|
16
|
+
|
|
17
|
+
**Fuso** is a terminal-native time tracker that lives in your terminal and gets out of your way. No accounts. No cloud. No config files to learn. You open it, press `s`, and you're tracking. Press `3` to switch to "Meeting." Press `s` again to stop. Done.
|
|
18
|
+
|
|
19
|
+
Your data is yours - plain JSON files in `~/.fuso/`.
|
|
20
|
+
|
|
21
|
+
## Screenshots
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<img src="assets/screenshot_tracking.png" alt="Fuso — active tracking" width="480">
|
|
25
|
+
</p>
|
|
26
|
+
<p align="center"><em>Tracking time with live category breakdown and per-category totals.</em></p>
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<img src="assets/screenshot_report.png" alt="Fuso — weekly report" width="480">
|
|
30
|
+
</p>
|
|
31
|
+
<p align="center"><em>Weekly and monthly reports with CSV export.</em></p>
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- **One-key workflow** — `s` to start/stop, `1`–`9` to switch categories, `p` to switch projects
|
|
36
|
+
- **Minimalist TUI** — only what matters is on screen, nothing else
|
|
37
|
+
- **Offline & private** — all data stored locally in `~/.fuso/` as plain JSON
|
|
38
|
+
- **Suspend-safe** — if your laptop sleeps or hibernates, the timer stops automatically at the last known time (works in WSL too)
|
|
39
|
+
- **CSV export** — weekly and monthly reports, ready for timesheets or invoicing
|
|
40
|
+
- **Zero config** — sensible defaults on first run, customize via the built-in settings screen
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
### Requirements
|
|
45
|
+
|
|
46
|
+
- **Ruby 3.2+** (Ruby 4.0+ recommended)
|
|
47
|
+
- A terminal with 256-color support
|
|
48
|
+
|
|
49
|
+
### From RubyGems
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
gem install fuso
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That's it. Now run `fuso` from anywhere.
|
|
56
|
+
|
|
57
|
+
### From source
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/AfonsoNeto/fuso-tui.git
|
|
61
|
+
cd fuso-tui
|
|
62
|
+
bundle install
|
|
63
|
+
bundle exec exe/fuso
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
### Quick Start
|
|
69
|
+
|
|
70
|
+
1. Launch `fuso`
|
|
71
|
+
2. Press `s` to start tracking
|
|
72
|
+
3. Press `1`–`9` to switch between categories
|
|
73
|
+
4. Press `s` to stop
|
|
74
|
+
5. Press `r` to view reports
|
|
75
|
+
|
|
76
|
+
That's it. No flags, no subcommands, no YAML.
|
|
77
|
+
|
|
78
|
+
### Typical Workflow
|
|
79
|
+
|
|
80
|
+
Imagine you're a developer working on two projects. You open Fuso in the morning:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
→ Press s to start tracking "Development" on "Project Alpha"
|
|
84
|
+
→ A meeting starts — press 3 to switch to "Meeting"
|
|
85
|
+
(Development time is saved automatically)
|
|
86
|
+
→ Meeting ends — press 1 to switch back to "Development"
|
|
87
|
+
→ Need to help a colleague — press 4 for "Support"
|
|
88
|
+
→ End of day — press s to stop
|
|
89
|
+
→ Press r then e to export your timesheet as CSV
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Every category switch is a single keypress. No interruptions. No context switching away from your terminal.
|
|
93
|
+
|
|
94
|
+
### Use Cases
|
|
95
|
+
|
|
96
|
+
| Scenario | How Fuso Helps |
|
|
97
|
+
|----------|---------------|
|
|
98
|
+
| **Freelancer billing** | Track hours per project/category, export CSV for invoicing |
|
|
99
|
+
| **Team standups** | Glance at yesterday's breakdown before standup |
|
|
100
|
+
| **Focus tracking** | See where your time actually goes vs. where you think it goes |
|
|
101
|
+
| **Contractor timesheets** | Monthly reports with weekly breakdown, ready for submission |
|
|
102
|
+
|
|
103
|
+
## Keyboard Shortcuts
|
|
104
|
+
|
|
105
|
+
### Main Screen
|
|
106
|
+
|
|
107
|
+
| Key | Action |
|
|
108
|
+
|-----|--------|
|
|
109
|
+
| `s` / `Space` | Start or stop the timer |
|
|
110
|
+
| `1`–`9` | Switch to category by number |
|
|
111
|
+
| `p` | Cycle through projects |
|
|
112
|
+
| `m` | Open settings (manage projects & categories) |
|
|
113
|
+
| `r` | Open reports |
|
|
114
|
+
| `q` / `Ctrl+C` | Quit (active session is saved automatically) |
|
|
115
|
+
|
|
116
|
+
### Settings Screen
|
|
117
|
+
|
|
118
|
+
| Key | Action |
|
|
119
|
+
|-----|--------|
|
|
120
|
+
| `↑` / `↓` | Navigate list |
|
|
121
|
+
| `Tab` | Switch between projects and categories |
|
|
122
|
+
| `a` | Add new item |
|
|
123
|
+
| `d` | Set selected item as default |
|
|
124
|
+
| `x` | Delete selected item |
|
|
125
|
+
| `Esc` | Back to main screen |
|
|
126
|
+
|
|
127
|
+
### Reports Screen
|
|
128
|
+
|
|
129
|
+
| Key | Action |
|
|
130
|
+
|-----|--------|
|
|
131
|
+
| `Tab` | Toggle between weekly and monthly view |
|
|
132
|
+
| `←` / `→` | Navigate between weeks or months |
|
|
133
|
+
| `e` | Export current view as CSV |
|
|
134
|
+
| `Esc` | Back to main screen |
|
|
135
|
+
|
|
136
|
+
## Reports
|
|
137
|
+
|
|
138
|
+
### In-App Reports
|
|
139
|
+
|
|
140
|
+
Press `r` to open the report view. Use `Tab` to switch between weekly and monthly breakdowns. Navigate with `←` / `→`.
|
|
141
|
+
|
|
142
|
+
### CSV Export
|
|
143
|
+
|
|
144
|
+
Press `e` on the report screen to export. Files are saved to `~/.fuso/exports/`:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
~/.fuso/exports/fuso_weekly_2026-W25.csv
|
|
148
|
+
~/.fuso/exports/fuso_monthly_2026-06.csv
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The monthly CSV includes a weekly breakdown with columns for each week-ending date, matching a standard timesheet format.
|
|
152
|
+
|
|
153
|
+
### CLI Reports
|
|
154
|
+
|
|
155
|
+
You can also view reports without the TUI:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
fuso report weekly # Current week summary
|
|
159
|
+
fuso report monthly # Current month summary
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Data Storage
|
|
163
|
+
|
|
164
|
+
All data lives in `~/.fuso/`:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
~/.fuso/
|
|
168
|
+
├── config.json # Projects, categories, defaults
|
|
169
|
+
├── sessions.json # All time entries, keyed by date
|
|
170
|
+
├── heartbeat # Active tracking timestamp (auto-cleaned)
|
|
171
|
+
└── exports/ # Exported CSV files
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Everything is plain JSON. You can read it, edit it, back it up, or sync it however you want.
|
|
175
|
+
|
|
176
|
+
## How Suspend Recovery Works
|
|
177
|
+
|
|
178
|
+
If your laptop goes to sleep, hibernates, or suspends — or if you close the lid and come back later — Fuso won't count the sleep time as work.
|
|
179
|
+
|
|
180
|
+
While the timer is running, Fuso writes a heartbeat timestamp to `~/.fuso/heartbeat` every 10 seconds. When it wakes up and notices the gap is too large (>60s), it saves the session at the last known heartbeat time and stops the timer.
|
|
181
|
+
|
|
182
|
+
This works everywhere: native Linux, WSL, macOS — no D-Bus, no systemd, no OS-specific APIs needed.
|
|
183
|
+
|
|
184
|
+
## Built With
|
|
185
|
+
|
|
186
|
+
- [Ruby](https://www.ruby-lang.org/) — simple, readable, gets the job done
|
|
187
|
+
- [Charm Ruby](https://charm-ruby.dev/) — `bubbletea` (Elm Architecture TUI), `lipgloss` (styling), `bubbles` (components)
|
|
188
|
+
|
|
189
|
+
## Contributing
|
|
190
|
+
|
|
191
|
+
Contributions are welcome! Whether it's bug fixes, new features, documentation improvements, or just ideas — all are appreciated.
|
|
192
|
+
|
|
193
|
+
### Getting Started
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Clone the repo
|
|
197
|
+
git clone https://github.com/AfonsoNeto/fuso-tui.git
|
|
198
|
+
cd fuso-tui
|
|
199
|
+
|
|
200
|
+
# Install dependencies
|
|
201
|
+
bundle install
|
|
202
|
+
|
|
203
|
+
# Run the test suite
|
|
204
|
+
bundle exec rake test
|
|
205
|
+
|
|
206
|
+
# Launch the app
|
|
207
|
+
bundle exec exe/fuso
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Running Tests
|
|
211
|
+
|
|
212
|
+
The project uses [Minitest](https://github.com/minitest/minitest). All tests run in an isolated temporary directory so your actual `~/.fuso/` data is never touched.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
bundle exec rake test
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Project Structure
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
lib/fuso/
|
|
222
|
+
├── app.rb # Main Bubbletea model (Elm Architecture)
|
|
223
|
+
├── config.rb # Config loading & persistence
|
|
224
|
+
├── session.rb # Time entry recording & queries
|
|
225
|
+
├── heartbeat.rb # Suspend detection via heartbeat file
|
|
226
|
+
├── styles.rb # Lipgloss style definitions
|
|
227
|
+
└── views/
|
|
228
|
+
├── main_view.rb # Timer screen (idle + tracking)
|
|
229
|
+
├── settings_view.rb # Project/category management
|
|
230
|
+
└── report_view.rb # Weekly & monthly reports + CSV export
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Guidelines
|
|
234
|
+
|
|
235
|
+
- Keep it minimal. Every feature should earn its screen space.
|
|
236
|
+
- Follow the Elm Architecture pattern (`init` → `update` → `view`).
|
|
237
|
+
- Add tests for new functionality.
|
|
238
|
+
- No external services, no network calls, no accounts.
|
|
239
|
+
|
|
240
|
+
## License
|
|
241
|
+
|
|
242
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/exe/fuso
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "fuso"
|
|
5
|
+
|
|
6
|
+
if ARGV.empty?
|
|
7
|
+
# Launch TUI
|
|
8
|
+
Bubbletea.run(Fuso::App.new, alt_screen: true)
|
|
9
|
+
elsif ARGV[0] == "report"
|
|
10
|
+
# CLI report mode
|
|
11
|
+
config = Fuso::Config.load
|
|
12
|
+
session = Fuso::Session.load
|
|
13
|
+
require "date"
|
|
14
|
+
|
|
15
|
+
case ARGV[1]
|
|
16
|
+
when "weekly"
|
|
17
|
+
date = Date.today
|
|
18
|
+
totals = session.weekly_breakdown(date)
|
|
19
|
+
monday = date - ((date.wday + 6) % 7)
|
|
20
|
+
sunday = monday + 6
|
|
21
|
+
|
|
22
|
+
puts "Fuso — Weekly Report"
|
|
23
|
+
puts "Week #{date.cweek}: #{monday.strftime('%b %d')} — #{sunday.strftime('%b %d, %Y')}"
|
|
24
|
+
puts ""
|
|
25
|
+
puts "#{'PROJECT / CATEGORY'.ljust(35)} #{'TOTAL'.rjust(8)}"
|
|
26
|
+
puts "─" * 45
|
|
27
|
+
|
|
28
|
+
grand_total = 0
|
|
29
|
+
totals.sort_by { |k, _| k }.each do |(project, category), seconds|
|
|
30
|
+
label = "#{project} / #{category}".ljust(35)
|
|
31
|
+
time = Fuso::Session.format_duration_hm(seconds).rjust(8)
|
|
32
|
+
puts "#{label} #{time}"
|
|
33
|
+
grand_total += seconds
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
puts "─" * 45
|
|
37
|
+
puts "#{'TOTAL'.ljust(35)} #{Fuso::Session.format_duration_hm(grand_total).rjust(8)}"
|
|
38
|
+
when "monthly"
|
|
39
|
+
date = Date.today
|
|
40
|
+
data = session.monthly_breakdown(date)
|
|
41
|
+
breakdown = data[:breakdown]
|
|
42
|
+
num_weeks = data[:num_weeks]
|
|
43
|
+
|
|
44
|
+
puts "Fuso — Monthly Report"
|
|
45
|
+
puts date.strftime("%B %Y")
|
|
46
|
+
puts ""
|
|
47
|
+
|
|
48
|
+
header = "PROJECT / CATEGORY".ljust(30)
|
|
49
|
+
(1..num_weeks).each { |w| header += "W#{w}".rjust(8) }
|
|
50
|
+
header += "TOTAL".rjust(10)
|
|
51
|
+
puts header
|
|
52
|
+
puts "─" * (30 + num_weeks * 8 + 10)
|
|
53
|
+
|
|
54
|
+
week_totals = Hash.new(0)
|
|
55
|
+
grand_total = 0
|
|
56
|
+
|
|
57
|
+
breakdown.sort_by { |k, _| k }.each do |(project, category), weeks|
|
|
58
|
+
label = "#{project} / #{category}"
|
|
59
|
+
label = label.length > 29 ? label[0..26] + "..." : label.ljust(30)
|
|
60
|
+
row = label
|
|
61
|
+
|
|
62
|
+
row_total = 0
|
|
63
|
+
(1..num_weeks).each do |w|
|
|
64
|
+
seconds = weeks[w]
|
|
65
|
+
row_total += seconds
|
|
66
|
+
week_totals[w] += seconds
|
|
67
|
+
time_str = seconds > 0 ? Fuso::Session.format_duration_hm(seconds) : "—"
|
|
68
|
+
row += time_str.rjust(8)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
grand_total += row_total
|
|
72
|
+
row += Fuso::Session.format_duration_hm(row_total).rjust(10)
|
|
73
|
+
puts row
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
puts "─" * (30 + num_weeks * 8 + 10)
|
|
77
|
+
total_row = "TOTAL".ljust(30)
|
|
78
|
+
(1..num_weeks).each do |w|
|
|
79
|
+
seconds = week_totals[w]
|
|
80
|
+
time_str = seconds > 0 ? Fuso::Session.format_duration_hm(seconds) : "—"
|
|
81
|
+
total_row += time_str.rjust(8)
|
|
82
|
+
end
|
|
83
|
+
total_row += Fuso::Session.format_duration_hm(grand_total).rjust(10)
|
|
84
|
+
puts total_row
|
|
85
|
+
else
|
|
86
|
+
puts "Usage: fuso report [weekly|monthly]"
|
|
87
|
+
end
|
|
88
|
+
else
|
|
89
|
+
puts "Usage: fuso [report weekly|monthly]"
|
|
90
|
+
end
|