vishnu-standard-time 0.1.0 → 0.2.0
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 +4 -4
- data/README.md +81 -2
- data/exe/vst +7 -0
- data/exe/vst.bak +6 -0
- data/lib/vishnu/standard_time/cli.rb +195 -0
- data/lib/vishnu/standard_time/rails_helpers.rb +24 -0
- data/lib/vishnu/standard_time/railtie.rb +17 -0
- data/lib/vishnu/standard_time.rb +361 -7
- data/vishnu-standard-time.gemspec +8 -4
- metadata +13 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d5ea2d413b3c79dc848318ee7ade65820616e070a588631f53b864f089ee641
|
|
4
|
+
data.tar.gz: 8934cbc3b798a5f3cb0bcd0d5b8a5a512170ddde50b3c61c2c68350334ced248
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9652dcd10723ab882e09d8738bf5a3c4e2349b2783814d8786404f6bea3f310247c8daf3f9594c697d755f012a0d02541ddcda7919c729ac0a66d7930f06cbdf
|
|
7
|
+
data.tar.gz: 6c6c3635ceba78fcf3b24c3560b31fdc4e902fdee328f7bbcbbf95e785f595164264326e0deeab2dae99260b1a47ce896114b7c2d2f412f1650f5f123b4c7a82
|
data/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# vishnu-standard-time
|
|
2
2
|
|
|
3
|
-
Ruby gem for **Vishnu Standard Time (VST)
|
|
3
|
+
Ruby gem for **Vishnu Standard Time (VST)** — a flow-based clock and productivity layer.
|
|
4
|
+
|
|
5
|
+
Official clock: https://time.vishnu.store
|
|
4
6
|
|
|
5
7
|
```bash
|
|
6
8
|
gem install vishnu-standard-time
|
|
@@ -9,6 +11,83 @@ gem install vishnu-standard-time
|
|
|
9
11
|
```ruby
|
|
10
12
|
require "vishnu/standard_time"
|
|
11
13
|
|
|
12
|
-
parts = Vishnu::StandardTime.
|
|
14
|
+
parts = Vishnu::StandardTime.local_now
|
|
13
15
|
puts Vishnu::StandardTime.format_vst(parts)
|
|
16
|
+
puts Vishnu::StandardTime.format_flows_left(parts)
|
|
17
|
+
puts Vishnu::StandardTime.standard_link
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Core idea
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
1 day = 259,200 flows
|
|
24
|
+
1 second = 3 flows
|
|
25
|
+
1 flow = 333⅓ ms
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
No AM/PM. No timezone drama. Just day progress and flows.
|
|
29
|
+
|
|
30
|
+
## Productivity helpers
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
Vishnu::StandardTime.flows_for(minutes: 25) # => 4500
|
|
34
|
+
Vishnu::StandardTime.minutes_for(flows: 900) # => 5.0
|
|
35
|
+
Vishnu::StandardTime.flows_left_today
|
|
36
|
+
Vishnu::StandardTime.percent_remaining
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Focus sessions
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
session = Vishnu::StandardTime.focus(flows: 4_500, label: "write README")
|
|
43
|
+
|
|
44
|
+
puts session.summary
|
|
45
|
+
puts session.flows_remaining
|
|
46
|
+
puts session.percent_complete
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Planning DSL
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
plan = Vishnu::StandardTime.plan do
|
|
53
|
+
block "Build", flows: 18_000
|
|
54
|
+
block "Break", minutes: 5
|
|
55
|
+
block "Ship", flows: 9_000
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
puts plan.summary
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Stamps for logs
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
puts Vishnu::StandardTime.stamp("started deploy")
|
|
65
|
+
# [VST D+000189 · 123,458 / 259,200 flows · 47.63% OF DAY] started deploy
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## CLI
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
vst now
|
|
72
|
+
vst now --json
|
|
73
|
+
vst percent
|
|
74
|
+
vst flows-left
|
|
75
|
+
vst stamp "published gem"
|
|
76
|
+
vst convert 25m
|
|
77
|
+
vst after 4500
|
|
78
|
+
vst until 180000
|
|
79
|
+
vst timer 4500 "break"
|
|
80
|
+
vst focus 25m "deep work"
|
|
81
|
+
vst prompt
|
|
82
|
+
vst badge vst.svg
|
|
83
|
+
vst link
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Rails helpers
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
# In a Rails view after requiring the gem:
|
|
90
|
+
<%= vst_clock %>
|
|
91
|
+
<%= vst_badge %>
|
|
92
|
+
<%= vst_flows_left %>
|
|
14
93
|
```
|
data/exe/vst
ADDED
data/exe/vst.bak
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../standard_time"
|
|
5
|
+
|
|
6
|
+
module Vishnu
|
|
7
|
+
module StandardTime
|
|
8
|
+
class CLI
|
|
9
|
+
def self.call(argv = ARGV, out = $stdout, err = $stderr)
|
|
10
|
+
status = new(argv, out, err).run
|
|
11
|
+
status.nil? ? 0 : Integer(status)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(argv, out, err)
|
|
15
|
+
@argv = argv.dup
|
|
16
|
+
@out = out
|
|
17
|
+
@err = err
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run
|
|
21
|
+
command = (@argv.shift || "now").to_s
|
|
22
|
+
|
|
23
|
+
case command
|
|
24
|
+
when "now"
|
|
25
|
+
now
|
|
26
|
+
when "percent"
|
|
27
|
+
percent
|
|
28
|
+
when "flows-left", "left"
|
|
29
|
+
flows_left
|
|
30
|
+
when "stamp"
|
|
31
|
+
stamp
|
|
32
|
+
when "convert"
|
|
33
|
+
convert
|
|
34
|
+
when "after"
|
|
35
|
+
after
|
|
36
|
+
when "until"
|
|
37
|
+
until_flow
|
|
38
|
+
when "timer"
|
|
39
|
+
timer(focus: false)
|
|
40
|
+
when "focus"
|
|
41
|
+
timer(focus: true)
|
|
42
|
+
when "prompt"
|
|
43
|
+
@out.puts Vishnu::StandardTime.format_prompt(Vishnu::StandardTime.local_now)
|
|
44
|
+
when "badge"
|
|
45
|
+
badge
|
|
46
|
+
when "link", "site"
|
|
47
|
+
@out.puts Vishnu::StandardTime.standard_link
|
|
48
|
+
when "help", "-h", "--help"
|
|
49
|
+
help
|
|
50
|
+
when "version", "-v", "--version"
|
|
51
|
+
@out.puts Vishnu::StandardTime::VERSION
|
|
52
|
+
else
|
|
53
|
+
@err.puts "Unknown command: #{command}"
|
|
54
|
+
help(@err)
|
|
55
|
+
1
|
|
56
|
+
end
|
|
57
|
+
rescue ArgumentError => e
|
|
58
|
+
@err.puts "vst: #{e.message}"
|
|
59
|
+
1
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def json?
|
|
65
|
+
@argv.delete("--json")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def precise?
|
|
69
|
+
@argv.delete("--precise")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def now
|
|
73
|
+
parts = Vishnu::StandardTime.local_now
|
|
74
|
+
if json?
|
|
75
|
+
@out.puts JSON.pretty_generate(Vishnu::StandardTime.to_h(parts))
|
|
76
|
+
else
|
|
77
|
+
@out.puts(precise? ? Vishnu::StandardTime.format_vst_precise(parts) : Vishnu::StandardTime.format_vst(parts))
|
|
78
|
+
end
|
|
79
|
+
0
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def percent
|
|
83
|
+
parts = Vishnu::StandardTime.local_now
|
|
84
|
+
@out.puts Vishnu::StandardTime.format_percent_of_day(parts)
|
|
85
|
+
@out.puts Vishnu::StandardTime.format_percent_remaining(parts)
|
|
86
|
+
0
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def flows_left
|
|
90
|
+
parts = Vishnu::StandardTime.local_now
|
|
91
|
+
@out.puts Vishnu::StandardTime.format_flows_left(parts)
|
|
92
|
+
0
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def stamp
|
|
96
|
+
message = @argv.join(" ")
|
|
97
|
+
@out.puts Vishnu::StandardTime.stamp(message)
|
|
98
|
+
0
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def convert
|
|
102
|
+
input = @argv.shift || raise(ArgumentError, "usage: vst convert 25m")
|
|
103
|
+
duration = Vishnu::StandardTime.parse_duration(input)
|
|
104
|
+
if json?
|
|
105
|
+
@out.puts JSON.pretty_generate(duration.to_h)
|
|
106
|
+
else
|
|
107
|
+
@out.puts "#{Vishnu::StandardTime.format_number(duration.flows)} flows"
|
|
108
|
+
@out.puts "#{format('%.2f', duration.seconds)} seconds"
|
|
109
|
+
@out.puts "#{format('%.2f', duration.minutes)} minutes"
|
|
110
|
+
end
|
|
111
|
+
0
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def after
|
|
115
|
+
input = @argv.shift || raise(ArgumentError, "usage: vst after 4500")
|
|
116
|
+
duration = Vishnu::StandardTime.parse_duration(input)
|
|
117
|
+
target = Vishnu::StandardTime.after(flows: duration.flows, from: Vishnu::StandardTime.local_now)
|
|
118
|
+
@out.puts Vishnu::StandardTime.format_vst(target)
|
|
119
|
+
0
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def until_flow
|
|
123
|
+
input = @argv.shift || raise(ArgumentError, "usage: vst until 180000")
|
|
124
|
+
result = Vishnu::StandardTime.until_flow(Integer(input), from: Vishnu::StandardTime.local_now)
|
|
125
|
+
if json?
|
|
126
|
+
@out.puts JSON.pretty_generate(result.to_h)
|
|
127
|
+
else
|
|
128
|
+
@out.puts "#{Vishnu::StandardTime.format_number(result.flows)} flows until #{input}"
|
|
129
|
+
@out.puts Vishnu::StandardTime.format_vst(result.target)
|
|
130
|
+
end
|
|
131
|
+
0
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def timer(focus:)
|
|
135
|
+
input = @argv.shift || raise(ArgumentError, "usage: vst #{focus ? 'focus' : 'timer'} 4500 [label]")
|
|
136
|
+
label = @argv.join(" ")
|
|
137
|
+
duration = Vishnu::StandardTime.parse_duration(input)
|
|
138
|
+
session = Vishnu::StandardTime.focus(flows: duration.flows, label: label.empty? ? (focus ? "focus" : "timer") : label)
|
|
139
|
+
|
|
140
|
+
@out.puts "#{focus ? 'FOCUS' : 'TIMER'} · #{session.label}"
|
|
141
|
+
@out.puts "Ends at: #{Vishnu::StandardTime.format_vst(session.ends_at)}"
|
|
142
|
+
|
|
143
|
+
interrupted = false
|
|
144
|
+
trap("INT") { interrupted = true }
|
|
145
|
+
|
|
146
|
+
until interrupted || session.complete?
|
|
147
|
+
remaining = session.flows_remaining
|
|
148
|
+
percent = session.percent_complete
|
|
149
|
+
@out.print "\r#{Vishnu::StandardTime.format_number(remaining)} flows left · #{format('%.2f%%', percent)} complete"
|
|
150
|
+
@out.flush
|
|
151
|
+
sleep 1
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
@out.puts
|
|
155
|
+
@out.puts(interrupted ? "Stopped." : "Done.")
|
|
156
|
+
interrupted ? 130 : 0
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def badge
|
|
160
|
+
path = @argv.shift
|
|
161
|
+
svg = Vishnu::StandardTime.svg_badge(Vishnu::StandardTime.local_now)
|
|
162
|
+
if path
|
|
163
|
+
File.write(path, svg)
|
|
164
|
+
@out.puts path
|
|
165
|
+
else
|
|
166
|
+
@out.puts svg
|
|
167
|
+
end
|
|
168
|
+
0
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def help(io = @out)
|
|
172
|
+
io.puts <<~HELP
|
|
173
|
+
Vishnu Standard Time CLI
|
|
174
|
+
|
|
175
|
+
Usage:
|
|
176
|
+
vst now [--json] [--precise]
|
|
177
|
+
vst percent
|
|
178
|
+
vst flows-left
|
|
179
|
+
vst stamp MESSAGE
|
|
180
|
+
vst convert 4500|25m|90s|1h [--json]
|
|
181
|
+
vst after 4500|25m|90s|1h
|
|
182
|
+
vst until FLOW [--json]
|
|
183
|
+
vst timer 4500|25m [label]
|
|
184
|
+
vst focus 4500|25m [label]
|
|
185
|
+
vst prompt
|
|
186
|
+
vst badge [file.svg]
|
|
187
|
+
vst link
|
|
188
|
+
|
|
189
|
+
#{Vishnu::StandardTime.standard_link}
|
|
190
|
+
HELP
|
|
191
|
+
0
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
require_relative "../standard_time"
|
|
5
|
+
|
|
6
|
+
module Vishnu
|
|
7
|
+
module StandardTime
|
|
8
|
+
module RailsHelpers
|
|
9
|
+
def vst_clock(parts = Vishnu::StandardTime.local_now)
|
|
10
|
+
html = %(<span class="vst-clock" data-vst-url="#{CGI.escapeHTML(Vishnu::StandardTime.website_url)}">#{CGI.escapeHTML(Vishnu::StandardTime.format_vst(parts))}</span>)
|
|
11
|
+
html.respond_to?(:html_safe) ? html.html_safe : html
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def vst_badge(parts = Vishnu::StandardTime.local_now)
|
|
15
|
+
html = %(<a class="vst-badge" href="#{CGI.escapeHTML(Vishnu::StandardTime.website_url)}" target="_blank" rel="noopener">#{CGI.escapeHTML(Vishnu::StandardTime.format_prompt(parts))}</a>)
|
|
16
|
+
html.respond_to?(:html_safe) ? html.html_safe : html
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def vst_flows_left(parts = Vishnu::StandardTime.local_now)
|
|
20
|
+
CGI.escapeHTML(Vishnu::StandardTime.format_flows_left(parts))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rails_helpers"
|
|
4
|
+
|
|
5
|
+
if defined?(Rails::Railtie)
|
|
6
|
+
module Vishnu
|
|
7
|
+
module StandardTime
|
|
8
|
+
class Railtie < Rails::Railtie
|
|
9
|
+
initializer "vishnu_standard_time.view_helpers" do
|
|
10
|
+
ActiveSupport.on_load(:action_view) do
|
|
11
|
+
include Vishnu::StandardTime::RailsHelpers
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/vishnu/standard_time.rb
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "cgi"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
3
6
|
module Vishnu
|
|
4
7
|
module StandardTime
|
|
5
|
-
|
|
8
|
+
VERSION = "0.2.0"
|
|
9
|
+
VST_SPEC_VERSION = VERSION
|
|
10
|
+
|
|
11
|
+
STANDARD_NAME = "Vishnu Standard Time"
|
|
12
|
+
WEBSITE_URL = "https://time.vishnu.store"
|
|
13
|
+
|
|
6
14
|
VST_EPOCH_UNIX_MS = 1_767_225_600_000
|
|
7
15
|
MS_PER_DAY = 86_400_000
|
|
8
16
|
FLOWS_PER_SECOND = 3
|
|
9
17
|
FLOWS_PER_MINUTE = 180
|
|
18
|
+
FLOWS_PER_HOUR = 10_800
|
|
10
19
|
FLOWS_PER_DAY = 259_200
|
|
11
20
|
FLOW_LENGTH_MS_TEXT = "333⅓"
|
|
21
|
+
|
|
22
|
+
# Legacy/compat display units kept for interop with early VST builds.
|
|
12
23
|
VICKS_PER_DAY = 100_000
|
|
13
24
|
MS_PER_VICK = 864
|
|
14
25
|
|
|
@@ -26,8 +37,172 @@ module Vishnu
|
|
|
26
37
|
keyword_init: true
|
|
27
38
|
)
|
|
28
39
|
|
|
40
|
+
Duration = Struct.new(
|
|
41
|
+
:flows,
|
|
42
|
+
:milliseconds,
|
|
43
|
+
:seconds,
|
|
44
|
+
:target,
|
|
45
|
+
keyword_init: true
|
|
46
|
+
) do
|
|
47
|
+
def minutes
|
|
48
|
+
seconds / 60.0
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def hours
|
|
52
|
+
seconds / 3600.0
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_h
|
|
56
|
+
{
|
|
57
|
+
flows: flows,
|
|
58
|
+
milliseconds: milliseconds,
|
|
59
|
+
seconds: seconds,
|
|
60
|
+
minutes: minutes,
|
|
61
|
+
hours: hours,
|
|
62
|
+
target: target && Vishnu::StandardTime.to_h(target)
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class FocusSession
|
|
68
|
+
attr_reader :label, :total_flows, :started_at, :ends_at
|
|
69
|
+
|
|
70
|
+
def initialize(total_flows:, label: nil, started_at: Vishnu::StandardTime.local_now)
|
|
71
|
+
@total_flows = Integer(total_flows)
|
|
72
|
+
raise ArgumentError, "total_flows must be positive" unless @total_flows.positive?
|
|
73
|
+
|
|
74
|
+
@label = label.to_s.strip.empty? ? "focus" : label.to_s
|
|
75
|
+
@started_at = started_at
|
|
76
|
+
@ends_at = Vishnu::StandardTime.after(flows: @total_flows, from: @started_at)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def flows_elapsed(now_parts = Vishnu::StandardTime.local_now)
|
|
80
|
+
elapsed_ms = Vishnu::StandardTime.unix_ms_for(now_parts) - Vishnu::StandardTime.unix_ms_for(started_at)
|
|
81
|
+
flows = Vishnu::StandardTime.flows_for(milliseconds: elapsed_ms)
|
|
82
|
+
[[flows, 0].max, total_flows].min
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def flows_remaining(now_parts = Vishnu::StandardTime.local_now)
|
|
86
|
+
[total_flows - flows_elapsed(now_parts), 0].max
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def percent_complete(now_parts = Vishnu::StandardTime.local_now)
|
|
90
|
+
(flows_elapsed(now_parts).to_f / total_flows) * 100
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def complete?(now_parts = Vishnu::StandardTime.local_now)
|
|
94
|
+
flows_remaining(now_parts).zero?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def to_h(now_parts = Vishnu::StandardTime.local_now)
|
|
98
|
+
{
|
|
99
|
+
label: label,
|
|
100
|
+
total_flows: total_flows,
|
|
101
|
+
flows_elapsed: flows_elapsed(now_parts),
|
|
102
|
+
flows_remaining: flows_remaining(now_parts),
|
|
103
|
+
percent_complete: percent_complete(now_parts).round(2),
|
|
104
|
+
started_at: Vishnu::StandardTime.to_h(started_at),
|
|
105
|
+
ends_at: Vishnu::StandardTime.to_h(ends_at),
|
|
106
|
+
complete: complete?(now_parts)
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def summary(now_parts = Vishnu::StandardTime.local_now)
|
|
111
|
+
"%s · %s / %s flows · %.2f%% complete" % [
|
|
112
|
+
label,
|
|
113
|
+
Vishnu::StandardTime.format_number(flows_elapsed(now_parts)),
|
|
114
|
+
Vishnu::StandardTime.format_number(total_flows),
|
|
115
|
+
percent_complete(now_parts)
|
|
116
|
+
]
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
class Plan
|
|
121
|
+
Block = Struct.new(:name, :flows, keyword_init: true) do
|
|
122
|
+
def minutes
|
|
123
|
+
Vishnu::StandardTime.minutes_for(flows: flows)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def percent_of_day
|
|
127
|
+
(flows.to_f / Vishnu::StandardTime::FLOWS_PER_DAY) * 100
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def to_h
|
|
131
|
+
{
|
|
132
|
+
name: name,
|
|
133
|
+
flows: flows,
|
|
134
|
+
minutes: minutes,
|
|
135
|
+
percent_of_day: percent_of_day.round(4)
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
attr_reader :blocks
|
|
141
|
+
|
|
142
|
+
def initialize
|
|
143
|
+
@blocks = []
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def block(name, flows: nil, seconds: nil, minutes: nil, hours: nil)
|
|
147
|
+
count = flows || Vishnu::StandardTime.flows_for(seconds: seconds, minutes: minutes, hours: hours)
|
|
148
|
+
count = Integer(count)
|
|
149
|
+
raise ArgumentError, "block flows must be positive" unless count.positive?
|
|
150
|
+
|
|
151
|
+
@blocks << Block.new(name: name.to_s, flows: count)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def total_flows
|
|
155
|
+
blocks.sum(&:flows)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def total_minutes
|
|
159
|
+
Vishnu::StandardTime.minutes_for(flows: total_flows)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def percent_of_day
|
|
163
|
+
(total_flows.to_f / Vishnu::StandardTime::FLOWS_PER_DAY) * 100
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def to_h
|
|
167
|
+
{
|
|
168
|
+
blocks: blocks.map(&:to_h),
|
|
169
|
+
total_flows: total_flows,
|
|
170
|
+
total_minutes: total_minutes,
|
|
171
|
+
percent_of_day: percent_of_day.round(4)
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def summary
|
|
176
|
+
width = [blocks.map { |b| b.name.length }.max || 4, 4].max
|
|
177
|
+
lines = blocks.map do |entry|
|
|
178
|
+
"%#{width}s %9s flows %7.2f min %6.2f%%" % [
|
|
179
|
+
entry.name,
|
|
180
|
+
Vishnu::StandardTime.format_number(entry.flows),
|
|
181
|
+
entry.minutes,
|
|
182
|
+
entry.percent_of_day
|
|
183
|
+
]
|
|
184
|
+
end
|
|
185
|
+
lines << ""
|
|
186
|
+
lines << "%#{width}s %9s flows %7.2f min %6.2f%%" % [
|
|
187
|
+
"Total",
|
|
188
|
+
Vishnu::StandardTime.format_number(total_flows),
|
|
189
|
+
total_minutes,
|
|
190
|
+
percent_of_day
|
|
191
|
+
]
|
|
192
|
+
lines.join("\n")
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
29
196
|
module_function
|
|
30
197
|
|
|
198
|
+
def website_url
|
|
199
|
+
WEBSITE_URL
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def standard_link
|
|
203
|
+
"#{STANDARD_NAME} — #{WEBSITE_URL}"
|
|
204
|
+
end
|
|
205
|
+
|
|
31
206
|
def from_unix_ms(unix_ms)
|
|
32
207
|
ms = Integer(unix_ms)
|
|
33
208
|
day, day_ms = (ms - VST_EPOCH_UNIX_MS).divmod(MS_PER_DAY)
|
|
@@ -39,11 +214,12 @@ module Vishnu
|
|
|
39
214
|
end
|
|
40
215
|
|
|
41
216
|
def parts_from_day_ms(day, day_ms, unix_ms: nil, source: "standard")
|
|
217
|
+
day_ms = Integer(day_ms)
|
|
42
218
|
raise ArgumentError, "day_ms must be from 0 through 86399999" if day_ms.negative? || day_ms >= MS_PER_DAY
|
|
43
219
|
|
|
44
220
|
flow = (day_ms * FLOWS_PER_DAY) / MS_PER_DAY
|
|
45
221
|
Parts.new(
|
|
46
|
-
day: day,
|
|
222
|
+
day: Integer(day),
|
|
47
223
|
day_ms: day_ms,
|
|
48
224
|
flow: flow,
|
|
49
225
|
flow_of_minute: flow % FLOWS_PER_MINUTE,
|
|
@@ -68,8 +244,15 @@ module Vishnu
|
|
|
68
244
|
parts_from_day_ms(day, day_ms, unix_ms: (time.to_f * 1000).floor, source: "local")
|
|
69
245
|
end
|
|
70
246
|
|
|
247
|
+
def unix_ms_for(parts)
|
|
248
|
+
return Integer(parts) if parts.is_a?(Integer)
|
|
249
|
+
return Integer(parts.unix_ms) if parts.respond_to?(:unix_ms) && parts.unix_ms
|
|
250
|
+
|
|
251
|
+
to_unix_ms(parts)
|
|
252
|
+
end
|
|
253
|
+
|
|
71
254
|
def to_unix_ms(parts)
|
|
72
|
-
VST_EPOCH_UNIX_MS + parts.day * MS_PER_DAY + parts.day_ms
|
|
255
|
+
VST_EPOCH_UNIX_MS + Integer(parts.day) * MS_PER_DAY + Integer(parts.day_ms)
|
|
73
256
|
end
|
|
74
257
|
|
|
75
258
|
def format_vst_day(parts)
|
|
@@ -77,13 +260,19 @@ module Vishnu
|
|
|
77
260
|
"D#{sign}#{parts.day.abs.to_s.rjust(6, "0")}"
|
|
78
261
|
end
|
|
79
262
|
|
|
263
|
+
def format_number(value, pad: false)
|
|
264
|
+
s = Integer(value).abs.to_s
|
|
265
|
+
s = s.rjust(6, "0") if pad
|
|
266
|
+
grouped = s.reverse.gsub(/(\d{3})(?=\d)/, "\\1,").reverse
|
|
267
|
+
Integer(value).negative? ? "-#{grouped}" : grouped
|
|
268
|
+
end
|
|
269
|
+
|
|
80
270
|
def comma6(value)
|
|
81
|
-
|
|
82
|
-
"#{s[0, 3]},#{s[3, 3]}"
|
|
271
|
+
format_number(value, pad: true)
|
|
83
272
|
end
|
|
84
273
|
|
|
85
274
|
def format_flow_count(parts)
|
|
86
|
-
"#{comma6(parts.flow)} /
|
|
275
|
+
"#{comma6(parts.flow)} / #{format_number(FLOWS_PER_DAY)} flows"
|
|
87
276
|
end
|
|
88
277
|
|
|
89
278
|
def format_flow_of_minute(parts)
|
|
@@ -94,6 +283,14 @@ module Vishnu
|
|
|
94
283
|
"%0#{digits + 3}.#{digits}f%% OF DAY" % parts.percent_of_day
|
|
95
284
|
end
|
|
96
285
|
|
|
286
|
+
def format_percent_remaining(parts, digits = 2)
|
|
287
|
+
"%0#{digits + 3}.#{digits}f%% DAY REMAINING" % percent_remaining(parts)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def format_flows_left(parts = local_now)
|
|
291
|
+
"#{format_number(flows_left_today(parts))} flows left today"
|
|
292
|
+
end
|
|
293
|
+
|
|
97
294
|
def format_vst(parts)
|
|
98
295
|
"VST #{format_vst_day(parts)} · #{format_flow_count(parts)} · #{format_percent_of_day(parts)}"
|
|
99
296
|
end
|
|
@@ -102,6 +299,152 @@ module Vishnu
|
|
|
102
299
|
"VST #{format_vst_day(parts)} · #{format_flow_of_minute(parts)} · #{format_flow_count(parts)} · #{format_percent_of_day(parts)}"
|
|
103
300
|
end
|
|
104
301
|
|
|
302
|
+
def format_prompt(parts = local_now)
|
|
303
|
+
"VST #{comma6(parts.flow)}/#{format_number(FLOWS_PER_DAY)} #{format('%.2f%%', parts.percent_of_day)}"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def flows_elapsed_today(parts = local_now)
|
|
307
|
+
Integer(parts.flow)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def flows_left_today(parts = local_now)
|
|
311
|
+
[FLOWS_PER_DAY - flows_elapsed_today(parts), 0].max
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def percent_remaining(parts = local_now)
|
|
315
|
+
[100.0 - parts.percent_of_day.to_f, 0.0].max
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def flows_for(seconds: nil, minutes: nil, hours: nil, days: nil, milliseconds: nil)
|
|
319
|
+
total_seconds = 0.0
|
|
320
|
+
total_seconds += seconds.to_f if seconds
|
|
321
|
+
total_seconds += minutes.to_f * 60 if minutes
|
|
322
|
+
total_seconds += hours.to_f * 3600 if hours
|
|
323
|
+
total_seconds += days.to_f * 86_400 if days
|
|
324
|
+
total_seconds += milliseconds.to_f / 1000 if milliseconds
|
|
325
|
+
(total_seconds * FLOWS_PER_SECOND).round
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def seconds_for(flows:)
|
|
329
|
+
flows.to_f / FLOWS_PER_SECOND
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def minutes_for(flows:)
|
|
333
|
+
seconds_for(flows: flows) / 60.0
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def hours_for(flows:)
|
|
337
|
+
seconds_for(flows: flows) / 3600.0
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def duration_ms_for(flows: nil, seconds: nil, minutes: nil, hours: nil, days: nil, milliseconds: nil)
|
|
341
|
+
ms = 0.0
|
|
342
|
+
ms += flows.to_f * 1000.0 / FLOWS_PER_SECOND if flows
|
|
343
|
+
ms += seconds.to_f * 1000 if seconds
|
|
344
|
+
ms += minutes.to_f * 60_000 if minutes
|
|
345
|
+
ms += hours.to_f * 3_600_000 if hours
|
|
346
|
+
ms += days.to_f * MS_PER_DAY if days
|
|
347
|
+
ms += milliseconds.to_f if milliseconds
|
|
348
|
+
ms.positive? ? ms.ceil : ms.floor
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def duration(flows: nil, seconds: nil, minutes: nil, hours: nil, days: nil, milliseconds: nil)
|
|
352
|
+
ms = duration_ms_for(
|
|
353
|
+
flows: flows,
|
|
354
|
+
seconds: seconds,
|
|
355
|
+
minutes: minutes,
|
|
356
|
+
hours: hours,
|
|
357
|
+
days: days,
|
|
358
|
+
milliseconds: milliseconds
|
|
359
|
+
)
|
|
360
|
+
Duration.new(
|
|
361
|
+
flows: flows || flows_for(milliseconds: ms),
|
|
362
|
+
milliseconds: ms,
|
|
363
|
+
seconds: ms / 1000.0
|
|
364
|
+
)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def parse_duration(input)
|
|
368
|
+
text = input.to_s.strip.downcase
|
|
369
|
+
raise ArgumentError, "duration is required" if text.empty?
|
|
370
|
+
|
|
371
|
+
case text
|
|
372
|
+
when /\A([+-]?\d+(?:\.\d+)?)\s*(f|flow|flows)?\z/
|
|
373
|
+
flows = Regexp.last_match(1).to_f
|
|
374
|
+
duration(flows: flows)
|
|
375
|
+
when /\A([+-]?\d+(?:\.\d+)?)\s*(ms|millisecond|milliseconds)\z/
|
|
376
|
+
duration(milliseconds: Regexp.last_match(1).to_f)
|
|
377
|
+
when /\A([+-]?\d+(?:\.\d+)?)\s*(s|sec|secs|second|seconds)\z/
|
|
378
|
+
duration(seconds: Regexp.last_match(1).to_f)
|
|
379
|
+
when /\A([+-]?\d+(?:\.\d+)?)\s*(m|min|mins|minute|minutes)\z/
|
|
380
|
+
duration(minutes: Regexp.last_match(1).to_f)
|
|
381
|
+
when /\A([+-]?\d+(?:\.\d+)?)\s*(h|hr|hrs|hour|hours)\z/
|
|
382
|
+
duration(hours: Regexp.last_match(1).to_f)
|
|
383
|
+
when /\A([+-]?\d+(?:\.\d+)?)\s*(d|day|days)\z/
|
|
384
|
+
duration(days: Regexp.last_match(1).to_f)
|
|
385
|
+
else
|
|
386
|
+
raise ArgumentError, "unknown duration '#{input}'. Try 4500, 4500f, 25m, 90s, 1h."
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def after(flows: nil, seconds: nil, minutes: nil, hours: nil, days: nil, milliseconds: nil, from: local_now)
|
|
391
|
+
ms = unix_ms_for(from) + duration_ms_for(
|
|
392
|
+
flows: flows,
|
|
393
|
+
seconds: seconds,
|
|
394
|
+
minutes: minutes,
|
|
395
|
+
hours: hours,
|
|
396
|
+
days: days,
|
|
397
|
+
milliseconds: milliseconds
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
from.respond_to?(:source) && from.source == "local" ? local_now(Time.at(ms / 1000.0)) : from_unix_ms(ms)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def until_flow(target_flow, from: local_now)
|
|
404
|
+
target = Integer(target_flow)
|
|
405
|
+
raise ArgumentError, "target flow must be from 0 through #{FLOWS_PER_DAY - 1}" if target.negative? || target >= FLOWS_PER_DAY
|
|
406
|
+
|
|
407
|
+
current = Integer(from.flow)
|
|
408
|
+
flow_delta = target >= current ? target - current : FLOWS_PER_DAY - current + target
|
|
409
|
+
ms = duration_ms_for(flows: flow_delta)
|
|
410
|
+
Duration.new(
|
|
411
|
+
flows: flow_delta,
|
|
412
|
+
milliseconds: ms,
|
|
413
|
+
seconds: ms / 1000.0,
|
|
414
|
+
target: after(flows: flow_delta, from: from)
|
|
415
|
+
)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def focus(flows: nil, seconds: nil, minutes: nil, hours: nil, label: nil, started_at: local_now)
|
|
419
|
+
total = flows || flows_for(seconds: seconds, minutes: minutes, hours: hours)
|
|
420
|
+
FocusSession.new(total_flows: total, label: label, started_at: started_at)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def plan(&block)
|
|
424
|
+
p = Plan.new
|
|
425
|
+
p.instance_eval(&block) if block
|
|
426
|
+
p
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def stamp(message = nil, parts = local_now)
|
|
430
|
+
suffix = message.to_s.strip
|
|
431
|
+
suffix.empty? ? "[#{format_vst(parts)}]" : "[#{format_vst(parts)}] #{suffix}"
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def svg_badge(parts = local_now, width: 560, height: 32)
|
|
435
|
+
text = CGI.escapeHTML(format_vst(parts))
|
|
436
|
+
url = CGI.escapeHTML(WEBSITE_URL)
|
|
437
|
+
<<~SVG.strip
|
|
438
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="#{Integer(width)}" height="#{Integer(height)}" role="img" aria-label="#{text}">
|
|
439
|
+
<title>#{text}</title>
|
|
440
|
+
<a href="#{url}" target="_blank">
|
|
441
|
+
<rect width="100%" height="100%" rx="6"/>
|
|
442
|
+
<text x="12" y="21" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13" fill="white">#{text}</text>
|
|
443
|
+
</a>
|
|
444
|
+
</svg>
|
|
445
|
+
SVG
|
|
446
|
+
end
|
|
447
|
+
|
|
105
448
|
def to_h(parts)
|
|
106
449
|
{
|
|
107
450
|
scale: "VST",
|
|
@@ -110,12 +453,23 @@ module Vishnu
|
|
|
110
453
|
flow: parts.flow,
|
|
111
454
|
flow_of_minute: parts.flow_of_minute,
|
|
112
455
|
flows_per_day: FLOWS_PER_DAY,
|
|
456
|
+
flows_left_today: flows_left_today(parts),
|
|
113
457
|
percent_of_day: parts.percent_of_day.round(6),
|
|
458
|
+
percent_remaining: percent_remaining(parts).round(6),
|
|
114
459
|
text: format_vst(parts),
|
|
460
|
+
precise_text: format_vst_precise(parts),
|
|
115
461
|
day_ms: parts.day_ms.to_s,
|
|
116
462
|
vick: parts.vick,
|
|
117
|
-
pulse: parts.pulse
|
|
463
|
+
pulse: parts.pulse,
|
|
464
|
+
website_url: WEBSITE_URL,
|
|
465
|
+
standard_link: standard_link
|
|
118
466
|
}
|
|
119
467
|
end
|
|
120
468
|
end
|
|
121
469
|
end
|
|
470
|
+
|
|
471
|
+
begin
|
|
472
|
+
require_relative "standard_time/railtie" if defined?(Rails)
|
|
473
|
+
rescue LoadError
|
|
474
|
+
# Rails is optional.
|
|
475
|
+
end
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
Gem::Specification.new do |spec|
|
|
2
2
|
spec.name = "vishnu-standard-time"
|
|
3
|
-
spec.version = "0.
|
|
4
|
-
spec.summary = "Vishnu Standard Time (VST):
|
|
3
|
+
spec.version = "0.2.0"
|
|
4
|
+
spec.summary = "Vishnu Standard Time (VST): flow-based time and productivity tools."
|
|
5
|
+
spec.description = "Vishnu Standard Time replaces AM/PM with 259,200 flows per day and ships helpers, a CLI, focus sessions, planning primitives, Rails helpers, and badges."
|
|
5
6
|
spec.authors = ["Vishnu"]
|
|
6
7
|
spec.license = "MIT"
|
|
7
8
|
spec.required_ruby_version = ">= 3.0"
|
|
8
|
-
spec.files = Dir["lib/**/*.rb", "README.md", "LICENSE*", "*.gemspec"]
|
|
9
|
+
spec.files = Dir["lib/**/*.rb", "exe/*", "README.md", "LICENSE*", "*.gemspec"]
|
|
9
10
|
spec.require_paths = ["lib"]
|
|
11
|
+
spec.bindir = "exe"
|
|
12
|
+
spec.executables = ["vst"]
|
|
10
13
|
spec.homepage = "https://time.vishnu.store"
|
|
11
14
|
spec.metadata = {
|
|
12
|
-
"
|
|
15
|
+
"homepage_uri" => "https://time.vishnu.store",
|
|
13
16
|
"changelog_uri" => "https://time.vishnu.store/.well-known/vst.json"
|
|
14
17
|
}
|
|
18
|
+
|
|
15
19
|
spec.add_development_dependency "minitest", "~> 5.25"
|
|
16
20
|
end
|
metadata
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vishnu-standard-time
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vishnu
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
@@ -23,18 +23,26 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '5.25'
|
|
26
|
-
|
|
26
|
+
description: Vishnu Standard Time replaces AM/PM with 259,200 flows per day and ships
|
|
27
|
+
helpers, a CLI, focus sessions, planning primitives, Rails helpers, and badges.
|
|
28
|
+
executables:
|
|
29
|
+
- vst
|
|
27
30
|
extensions: []
|
|
28
31
|
extra_rdoc_files: []
|
|
29
32
|
files:
|
|
30
33
|
- README.md
|
|
34
|
+
- exe/vst
|
|
35
|
+
- exe/vst.bak
|
|
31
36
|
- lib/vishnu/standard_time.rb
|
|
37
|
+
- lib/vishnu/standard_time/cli.rb
|
|
38
|
+
- lib/vishnu/standard_time/rails_helpers.rb
|
|
39
|
+
- lib/vishnu/standard_time/railtie.rb
|
|
32
40
|
- vishnu-standard-time.gemspec
|
|
33
41
|
homepage: https://time.vishnu.store
|
|
34
42
|
licenses:
|
|
35
43
|
- MIT
|
|
36
44
|
metadata:
|
|
37
|
-
|
|
45
|
+
homepage_uri: https://time.vishnu.store
|
|
38
46
|
changelog_uri: https://time.vishnu.store/.well-known/vst.json
|
|
39
47
|
rdoc_options: []
|
|
40
48
|
require_paths:
|
|
@@ -52,5 +60,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
52
60
|
requirements: []
|
|
53
61
|
rubygems_version: 3.6.9
|
|
54
62
|
specification_version: 4
|
|
55
|
-
summary: 'Vishnu Standard Time (VST):
|
|
63
|
+
summary: 'Vishnu Standard Time (VST): flow-based time and productivity tools.'
|
|
56
64
|
test_files: []
|