mdlogbook 0.1.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 +7 -0
- data/AGENTS.md +80 -0
- data/CHANGELOG.md +16 -0
- data/CLAUDE.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +99 -0
- data/Rakefile +21 -0
- data/docs/sample-logs/sample-2025-01.md +122 -0
- data/docs/sample-logs/sample-2025-02.md +123 -0
- data/docs/sample-logs/sample-2025-03.md +116 -0
- data/docs/sample-logs/sample-2025-04.md +128 -0
- data/docs/sample-logs/sample-2025-05.md +122 -0
- data/docs/sample-logs/sample-2025-06.md +113 -0
- data/docs/sample-logs/sample-2025-07.md +122 -0
- data/docs/sample-logs/sample-2025-08.md +94 -0
- data/docs/sample-logs/sample-2025-09.md +119 -0
- data/docs/sample-logs/sample-2025-10.md +123 -0
- data/docs/sample-logs/sample-2025-11.md +113 -0
- data/docs/sample-logs/sample-2025-12.md +121 -0
- data/docs/usage.md +778 -0
- data/exe/mlb +6 -0
- data/lib/mdlogbook/arg_parser.rb +372 -0
- data/lib/mdlogbook/brace_expander.rb +59 -0
- data/lib/mdlogbook/cli.rb +224 -0
- data/lib/mdlogbook/config.rb +247 -0
- data/lib/mdlogbook/date_filter.rb +22 -0
- data/lib/mdlogbook/editor_launcher.rb +90 -0
- data/lib/mdlogbook/env.rb +95 -0
- data/lib/mdlogbook/file_resolver.rb +43 -0
- data/lib/mdlogbook/formatter/anonymize.rb +75 -0
- data/lib/mdlogbook/formatter/anonymize_dict.rb +36 -0
- data/lib/mdlogbook/formatter/base.rb +138 -0
- data/lib/mdlogbook/formatter/calendar.rb +154 -0
- data/lib/mdlogbook/formatter/chart_series_builder.rb +57 -0
- data/lib/mdlogbook/formatter/extract.rb +41 -0
- data/lib/mdlogbook/formatter/grid.rb +256 -0
- data/lib/mdlogbook/formatter/heatmap.rb +142 -0
- data/lib/mdlogbook/formatter/line_chart.rb +156 -0
- data/lib/mdlogbook/formatter/simple.rb +42 -0
- data/lib/mdlogbook/option_def.rb +68 -0
- data/lib/mdlogbook/option_registry.rb +205 -0
- data/lib/mdlogbook/overlap_checker.rb +42 -0
- data/lib/mdlogbook/parser.rb +213 -0
- data/lib/mdlogbook/period_spec.rb +56 -0
- data/lib/mdlogbook/raw_extractor.rb +100 -0
- data/lib/mdlogbook/runner/base.rb +66 -0
- data/lib/mdlogbook/runner/editor_runner.rb +49 -0
- data/lib/mdlogbook/runner/extract_format_runner.rb +54 -0
- data/lib/mdlogbook/runner/standard_format_runner.rb +105 -0
- data/lib/mdlogbook/runner/visual_format_runner.rb +77 -0
- data/lib/mdlogbook/runner.rb +27 -0
- data/lib/mdlogbook/section_extractor.rb +140 -0
- data/lib/mdlogbook/version.rb +6 -0
- data/lib/mdlogbook/work_thresholds.rb +37 -0
- data/lib/mdlogbook.rb +41 -0
- data/sig/mdlogbook/arg_parser.rbs +54 -0
- data/sig/mdlogbook/brace_expander.rbs +4 -0
- data/sig/mdlogbook/cli.rbs +14 -0
- data/sig/mdlogbook/config.rbs +60 -0
- data/sig/mdlogbook/date_filter.rbs +4 -0
- data/sig/mdlogbook/editor_launcher.rbs +9 -0
- data/sig/mdlogbook/env.rbs +32 -0
- data/sig/mdlogbook/file_resolver.rbs +9 -0
- data/sig/mdlogbook/formatter/anonymize.rbs +4 -0
- data/sig/mdlogbook/formatter/anonymize_dict.rbs +4 -0
- data/sig/mdlogbook/formatter/base.rbs +15 -0
- data/sig/mdlogbook/formatter/calendar.rbs +4 -0
- data/sig/mdlogbook/formatter/chart_series_builder.rbs +9 -0
- data/sig/mdlogbook/formatter/extract.rbs +4 -0
- data/sig/mdlogbook/formatter/grid.rbs +8 -0
- data/sig/mdlogbook/formatter/heatmap.rbs +10 -0
- data/sig/mdlogbook/formatter/line_chart.rbs +7 -0
- data/sig/mdlogbook/formatter/simple.rbs +6 -0
- data/sig/mdlogbook/option_def.rbs +30 -0
- data/sig/mdlogbook/option_registry.rbs +18 -0
- data/sig/mdlogbook/overlap_checker.rbs +13 -0
- data/sig/mdlogbook/parser.rbs +32 -0
- data/sig/mdlogbook/period_spec.rbs +11 -0
- data/sig/mdlogbook/raw_extractor.rbs +15 -0
- data/sig/mdlogbook/runner/base.rbs +4 -0
- data/sig/mdlogbook/runner/editor_runner.rbs +5 -0
- data/sig/mdlogbook/runner/extract_format_runner.rbs +4 -0
- data/sig/mdlogbook/runner/standard_format_runner.rbs +4 -0
- data/sig/mdlogbook/runner/visual_format_runner.rbs +4 -0
- data/sig/mdlogbook/runner.rbs +4 -0
- data/sig/mdlogbook/section_extractor.rbs +24 -0
- data/sig/mdlogbook/work_thresholds.rbs +11 -0
- data/sig/mdlogbook.rbs +8 -0
- metadata +149 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mdlogbook
|
|
4
|
+
module Runner
|
|
5
|
+
# Runs visual output modes: line chart, grid, and heatmap.
|
|
6
|
+
class VisualFormatRunner < Base
|
|
7
|
+
def run
|
|
8
|
+
if @env.line_chart_format?
|
|
9
|
+
run_line_chart
|
|
10
|
+
elsif @env.grid_format?
|
|
11
|
+
run_grid
|
|
12
|
+
elsif @env.heatmap_format?
|
|
13
|
+
run_heatmap
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def run_line_chart
|
|
20
|
+
unless all_month_unit?
|
|
21
|
+
raise ArgumentError, "--line-chart requires month-unit specifiers (e.g. 2026/1, m0, q0)"
|
|
22
|
+
end
|
|
23
|
+
if @specs.size > 3
|
|
24
|
+
raise ArgumentError, "--line-chart supports at most 3 months (got #{@specs.size})"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
formatter = Formatter::LineChart.new
|
|
28
|
+
each_sorted_spec.with_index do |spec, index|
|
|
29
|
+
@env.puts if index.positive?
|
|
30
|
+
result = load_result(spec)
|
|
31
|
+
formatter.format_output(
|
|
32
|
+
result, work_threshold, @env.stdout,
|
|
33
|
+
spec: spec,
|
|
34
|
+
size: @env.opts.line_chart_size,
|
|
35
|
+
today: @env.today,
|
|
36
|
+
line_chart_config: line_chart_config
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def run_grid
|
|
42
|
+
unless all_month_unit?
|
|
43
|
+
raise ArgumentError, "--grid requires month-unit specifiers (e.g. 2026/1, m0, q0)"
|
|
44
|
+
end
|
|
45
|
+
if @specs.size > 12
|
|
46
|
+
raise ArgumentError, "--grid supports at most 12 months (got #{@specs.size})"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
threshold = work_threshold
|
|
50
|
+
formatter = Formatter::Grid.new
|
|
51
|
+
spec_results = each_sorted_spec.map { |spec| [spec, load_result(spec)] }
|
|
52
|
+
formatter.format_output(spec_results, threshold, @env.stdout, today: @env.today, line_chart_config: line_chart_config)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def run_heatmap
|
|
56
|
+
merged = Parser::Result.new
|
|
57
|
+
each_spec { |spec| merged.merge!(load_result(spec)) }
|
|
58
|
+
|
|
59
|
+
range_begin, range_end = spec_range
|
|
60
|
+
formatter = Formatter::Heatmap.new
|
|
61
|
+
formatter.format_output(merged, @env.stdout,
|
|
62
|
+
resolution: @env.opts.heatmap_resolution,
|
|
63
|
+
heatmap_config: @env.config.heatmap_settings,
|
|
64
|
+
range_begin: range_begin,
|
|
65
|
+
range_end: range_end)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def all_month_unit?
|
|
69
|
+
each_spec.all?(&:month_unit?)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def line_chart_config
|
|
73
|
+
@env.config.visual_settings
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mdlogbook
|
|
4
|
+
# Executes the resolved command after CLI has parsed options and loaded config.
|
|
5
|
+
#
|
|
6
|
+
# Responsible for dispatching to the appropriate output mode (summary views,
|
|
7
|
+
# line chart, grid, heatmap, raw extraction, grep, or editor launch) based
|
|
8
|
+
# on the parsed option flags.
|
|
9
|
+
module Runner
|
|
10
|
+
class << self
|
|
11
|
+
def dispatch(env:)
|
|
12
|
+
resolver = FileResolver.new(config: env.config, basedir: env.opts.path_override)
|
|
13
|
+
runner_class =
|
|
14
|
+
if env.edit?
|
|
15
|
+
EditorRunner
|
|
16
|
+
elsif env.extract_format?
|
|
17
|
+
ExtractFormatRunner
|
|
18
|
+
elsif env.visual_format?
|
|
19
|
+
VisualFormatRunner
|
|
20
|
+
else
|
|
21
|
+
StandardFormatRunner
|
|
22
|
+
end
|
|
23
|
+
runner_class.new(env: env, file_resolver: resolver).run
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module Mdlogbook
|
|
6
|
+
# Extracts markdown sections whose level-2 or level-3 headings match a regexp.
|
|
7
|
+
class SectionExtractor
|
|
8
|
+
DATE_HEADER_RE = /^# (\d{4}-\d{2}-\d{2})$/
|
|
9
|
+
SECTION_HEADER_RE = /^(##|###) (.+)$/
|
|
10
|
+
|
|
11
|
+
# Immutable value object representing one extracted markdown section.
|
|
12
|
+
Section = Data.define(:date, :heading_level, :heading, :lines)
|
|
13
|
+
|
|
14
|
+
# Accumulates extracted sections in input order.
|
|
15
|
+
class Result
|
|
16
|
+
# @return [Array[Section]]
|
|
17
|
+
attr_reader :sections
|
|
18
|
+
|
|
19
|
+
# Creates an empty result.
|
|
20
|
+
def initialize
|
|
21
|
+
@sections = []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Adds a section to the result.
|
|
25
|
+
# @param section [Section]
|
|
26
|
+
# @return [void]
|
|
27
|
+
def add_section(section)
|
|
28
|
+
@sections << section
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Merges another {Result} into this one in place.
|
|
32
|
+
# @param other [Result]
|
|
33
|
+
# @return [self]
|
|
34
|
+
def merge!(other)
|
|
35
|
+
@sections.concat(other.sections)
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Iterates over extracted sections in input order.
|
|
40
|
+
# @yield [section]
|
|
41
|
+
# @yieldparam section [Section]
|
|
42
|
+
# @return [void]
|
|
43
|
+
def each_section(&)
|
|
44
|
+
@sections.each(&)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns true when no sections have been added.
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
def empty?
|
|
50
|
+
@sections.empty?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Parses logbook content from a file path, extracting matching sections within +date_range+.
|
|
55
|
+
# @param filename [String]
|
|
56
|
+
# @param date_range [Range<Date>]
|
|
57
|
+
# @param regexp [Regexp]
|
|
58
|
+
# @return [Result]
|
|
59
|
+
def extract_file(filename, date_range, regexp:)
|
|
60
|
+
File.open(filename, "r") { |io| extract(io, date_range, regexp: regexp) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Parses logbook content from an IO object, extracting matching sections within +date_range+.
|
|
64
|
+
# @param io [IO]
|
|
65
|
+
# @param date_range [Range<Date>]
|
|
66
|
+
# @param regexp [Regexp]
|
|
67
|
+
# @return [Result]
|
|
68
|
+
def extract(io, date_range, regexp:)
|
|
69
|
+
result = Result.new
|
|
70
|
+
current_date = nil
|
|
71
|
+
current_date_header = nil
|
|
72
|
+
current_section = nil
|
|
73
|
+
|
|
74
|
+
io.each_line do |raw_line|
|
|
75
|
+
line = raw_line.chomp
|
|
76
|
+
|
|
77
|
+
if (date_match = DATE_HEADER_RE.match(line))
|
|
78
|
+
flush_section(result, current_section, date_range)
|
|
79
|
+
current_date = Date.parse(date_match[1])
|
|
80
|
+
current_date_header = line
|
|
81
|
+
current_section = nil
|
|
82
|
+
next
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
header_match = SECTION_HEADER_RE.match(line)
|
|
86
|
+
if header_match
|
|
87
|
+
current_section = advance_section(result, current_section, current_date, current_date_header, date_range, regexp, line, header_match)
|
|
88
|
+
next unless current_section
|
|
89
|
+
|
|
90
|
+
current_section[:lines] << line if nested_heading?(current_section, header_match)
|
|
91
|
+
next
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
next unless current_section
|
|
95
|
+
|
|
96
|
+
current_section[:lines] << line
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
flush_section(result, current_section, date_range)
|
|
100
|
+
result
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def advance_section(result, current_section, current_date, current_date_header, date_range, regexp, line, header_match)
|
|
106
|
+
level = header_match[1].length
|
|
107
|
+
heading = header_match[2]
|
|
108
|
+
|
|
109
|
+
if current_section && level <= current_section[:heading_level]
|
|
110
|
+
flush_section(result, current_section, date_range)
|
|
111
|
+
current_section = nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
return current_section unless current_date && regexp.match?(heading)
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
date: current_date,
|
|
118
|
+
heading_level: level,
|
|
119
|
+
heading: heading,
|
|
120
|
+
lines: [current_date_header, "", line]
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def flush_section(result, current_section, date_range)
|
|
125
|
+
return unless current_section
|
|
126
|
+
return unless date_range.cover?(current_section[:date])
|
|
127
|
+
|
|
128
|
+
result.add_section(Section.new(
|
|
129
|
+
date: current_section[:date],
|
|
130
|
+
heading_level: current_section[:heading_level],
|
|
131
|
+
heading: current_section[:heading],
|
|
132
|
+
lines: current_section[:lines].dup
|
|
133
|
+
))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def nested_heading?(current_section, header_match)
|
|
137
|
+
header_match[1].length > current_section[:heading_level]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mdlogbook
|
|
4
|
+
# Holds working-time thresholds derived from monthly targets.
|
|
5
|
+
class WorkThresholds
|
|
6
|
+
# @!attribute [r] target_hours
|
|
7
|
+
# @return [Numeric] Monthly target hours.
|
|
8
|
+
# @!attribute [r] working_days_per_month
|
|
9
|
+
# @return [Numeric] Assumed number of working days per month.
|
|
10
|
+
# @!attribute [r] daily_target
|
|
11
|
+
# @return [Float] Target hours per working day.
|
|
12
|
+
# @!attribute [r] high_threshold
|
|
13
|
+
# @return [Float] Upper threshold (120% of daily_target) for "excellent" rating.
|
|
14
|
+
# @!attribute [r] low_threshold
|
|
15
|
+
# @return [Float] Lower threshold (70% of daily_target) for "poor" rating.
|
|
16
|
+
attr_reader :target_hours, :working_days_per_month,
|
|
17
|
+
:daily_target, :high_threshold, :low_threshold
|
|
18
|
+
|
|
19
|
+
# @param target_hours [Numeric, nil] Monthly target hours, or +nil+ for no target.
|
|
20
|
+
# @param working_days_per_month [Numeric] Assumed working days per month (default: 20).
|
|
21
|
+
def initialize(target_hours:, working_days_per_month: 20)
|
|
22
|
+
@target_hours = target_hours
|
|
23
|
+
@working_days_per_month = working_days_per_month
|
|
24
|
+
if target_hours
|
|
25
|
+
@daily_target = target_hours.to_f / working_days_per_month
|
|
26
|
+
@high_threshold = @daily_target * 1.2
|
|
27
|
+
@low_threshold = @daily_target * 0.7
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns +true+ when a monthly target is configured.
|
|
32
|
+
# @return [Boolean]
|
|
33
|
+
def target_hours?
|
|
34
|
+
!@target_hours.nil?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/mdlogbook.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Top-level namespace for the mdlogbook gem.
|
|
4
|
+
module Mdlogbook
|
|
5
|
+
# Base error class for mdlogbook-specific exceptions.
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require_relative "mdlogbook/version"
|
|
10
|
+
require_relative "mdlogbook/config"
|
|
11
|
+
require_relative "mdlogbook/parser"
|
|
12
|
+
require_relative "mdlogbook/work_thresholds"
|
|
13
|
+
require_relative "mdlogbook/file_resolver"
|
|
14
|
+
require_relative "mdlogbook/period_spec"
|
|
15
|
+
require_relative "mdlogbook/brace_expander"
|
|
16
|
+
require_relative "mdlogbook/arg_parser"
|
|
17
|
+
require_relative "mdlogbook/date_filter"
|
|
18
|
+
require_relative "mdlogbook/editor_launcher"
|
|
19
|
+
require_relative "mdlogbook/overlap_checker"
|
|
20
|
+
require_relative "mdlogbook/raw_extractor"
|
|
21
|
+
require_relative "mdlogbook/section_extractor"
|
|
22
|
+
require_relative "mdlogbook/formatter/base"
|
|
23
|
+
require_relative "mdlogbook/formatter/simple"
|
|
24
|
+
require_relative "mdlogbook/formatter/calendar"
|
|
25
|
+
require_relative "mdlogbook/formatter/anonymize"
|
|
26
|
+
require_relative "mdlogbook/formatter/anonymize_dict"
|
|
27
|
+
require_relative "mdlogbook/formatter/extract"
|
|
28
|
+
require_relative "mdlogbook/formatter/chart_series_builder"
|
|
29
|
+
require_relative "mdlogbook/formatter/line_chart"
|
|
30
|
+
require_relative "mdlogbook/formatter/grid"
|
|
31
|
+
require_relative "mdlogbook/formatter/heatmap"
|
|
32
|
+
require_relative "mdlogbook/runner"
|
|
33
|
+
require_relative "mdlogbook/runner/base"
|
|
34
|
+
require_relative "mdlogbook/runner/editor_runner"
|
|
35
|
+
require_relative "mdlogbook/runner/extract_format_runner"
|
|
36
|
+
require_relative "mdlogbook/runner/visual_format_runner"
|
|
37
|
+
require_relative "mdlogbook/runner/standard_format_runner"
|
|
38
|
+
require_relative "mdlogbook/env"
|
|
39
|
+
require_relative "mdlogbook/option_def"
|
|
40
|
+
require_relative "mdlogbook/option_registry"
|
|
41
|
+
require_relative "mdlogbook/cli"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Parses command-line arguments into an ordered list of PeriodSpec objects.
|
|
2
|
+
class Mdlogbook::ArgParser
|
|
3
|
+
class Directive
|
|
4
|
+
def resolve_with: (ArgParser::DirectiveContext) -> Array[Mdlogbook::PeriodSpec]
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class DateDirective < Directive
|
|
8
|
+
def initialize: (Date) -> void
|
|
9
|
+
def resolve_with: (ArgParser::DirectiveContext) -> Array[Mdlogbook::PeriodSpec]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class MonthDirective < Directive
|
|
13
|
+
def initialize: (Integer, Integer) -> void
|
|
14
|
+
def resolve_with: (ArgParser::DirectiveContext) -> Array[Mdlogbook::PeriodSpec]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class RelativeDurationDirective < Directive
|
|
18
|
+
def initialize: (Integer) -> void
|
|
19
|
+
def resolve_with: (ArgParser::DirectiveContext) -> Array[Mdlogbook::PeriodSpec]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class RelativePointDirective < Directive
|
|
23
|
+
def initialize: (String, Integer) -> void
|
|
24
|
+
def resolve_with: (ArgParser::DirectiveContext) -> Array[Mdlogbook::PeriodSpec]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class RelativeRangeDirective < Directive
|
|
28
|
+
def initialize: (String, Integer, Integer) -> void
|
|
29
|
+
def resolve_with: (ArgParser::DirectiveContext) -> Array[Mdlogbook::PeriodSpec]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class RelativeOpenEndedDirective < Directive
|
|
33
|
+
def initialize: (String, Integer) -> void
|
|
34
|
+
def resolve_with: (ArgParser::DirectiveContext) -> Array[Mdlogbook::PeriodSpec]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class WildcardDirective < Directive
|
|
38
|
+
def initialize: (String) -> void
|
|
39
|
+
def resolve_with: (ArgParser::DirectiveContext) -> Array[Mdlogbook::PeriodSpec]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class DirectiveContext
|
|
43
|
+
attr_reader today: Date
|
|
44
|
+
attr_reader expander: Mdlogbook::BraceExpander
|
|
45
|
+
|
|
46
|
+
def initialize: (parser: ArgParser, today: Date, file_resolver: Mdlogbook::FileResolver?, expander: Mdlogbook::BraceExpander) -> void
|
|
47
|
+
def resolve: (ArgParser::Directive) -> Array[Mdlogbook::PeriodSpec]
|
|
48
|
+
def list_available_months: () -> Array[[Integer, Integer]]
|
|
49
|
+
def new_parser: () -> ArgParser
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def initialize: (?today: Date, ?file_resolver: Mdlogbook::FileResolver?) -> void
|
|
53
|
+
def parse: (Array[String]) -> Array[Mdlogbook::PeriodSpec]
|
|
54
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Command-line interface for mlb.
|
|
2
|
+
class Mdlogbook::CLI
|
|
3
|
+
def self.init_template: (?path: String?) -> String
|
|
4
|
+
|
|
5
|
+
def initialize: (?argv: Array[String], ?stdout: IO, ?stderr: IO, ?today: Date) -> void
|
|
6
|
+
def run: () -> bool
|
|
7
|
+
def debug: () -> self
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def print_completion: (String shell) -> void
|
|
12
|
+
def parse_options!: () -> void
|
|
13
|
+
def build_registry: () -> Mdlogbook::OptionRegistry
|
|
14
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Loads per-project configuration from a YAML file.
|
|
2
|
+
class Mdlogbook::Config
|
|
3
|
+
DEFAULT_PROJECT: String
|
|
4
|
+
DEFAULT_ANONYMIZE_FORMAT: String
|
|
5
|
+
DEFAULT_HEATMAP_START_HOUR: Integer
|
|
6
|
+
DEFAULT_VISUAL_SETTINGS: Hash[String, untyped]
|
|
7
|
+
VALID_VISUAL_COLORS: Array[Symbol]
|
|
8
|
+
|
|
9
|
+
# Immutable value object holding anonymize formatter settings.
|
|
10
|
+
class AnonymizeSettings < Data
|
|
11
|
+
attr_reader project: String
|
|
12
|
+
attr_reader format: String
|
|
13
|
+
attr_reader salt: String
|
|
14
|
+
attr_reader passthrough: Array[String]
|
|
15
|
+
attr_reader preamble: String?
|
|
16
|
+
attr_reader postamble: String?
|
|
17
|
+
|
|
18
|
+
def initialize: (project: String, format: String, salt: String, passthrough: Array[String], preamble: String?, postamble: String?) -> void
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Immutable value object holding visual formatter settings.
|
|
22
|
+
class VisualSettings < Data
|
|
23
|
+
attr_reader show_today: bool
|
|
24
|
+
attr_reader show_weekday: bool
|
|
25
|
+
attr_reader show_linear: bool
|
|
26
|
+
attr_reader colors: Hash[String, Symbol]
|
|
27
|
+
|
|
28
|
+
def initialize: (?show_today: bool, ?show_weekday: bool, ?show_linear: bool, ?colors: Hash[String, Symbol]) -> void
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Immutable value object holding heatmap formatter settings.
|
|
32
|
+
class HeatmapSettings < Data
|
|
33
|
+
attr_reader start_hour: Integer
|
|
34
|
+
|
|
35
|
+
def initialize: (start_hour: Integer) -> void
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Immutable value object holding extract formatter settings.
|
|
39
|
+
class ExtractSettings < Data
|
|
40
|
+
attr_reader preamble: String?
|
|
41
|
+
attr_reader postamble: String?
|
|
42
|
+
|
|
43
|
+
def initialize: (preamble: String?, postamble: String?) -> void
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
attr_reader config_path: String
|
|
47
|
+
|
|
48
|
+
def self.default_config_path: () -> String
|
|
49
|
+
def initialize: (?config_path: String?, ?project: String?) -> void
|
|
50
|
+
def current_project: () -> String
|
|
51
|
+
def projects: () -> Array[String]
|
|
52
|
+
def basedir: (?String) -> String
|
|
53
|
+
def target_hours: (?String) -> Numeric?
|
|
54
|
+
def default_view: (?String) -> String?
|
|
55
|
+
def extract_settings: (?String) -> Config::ExtractSettings
|
|
56
|
+
def anonymize_settings: (?String) -> Config::AnonymizeSettings
|
|
57
|
+
def heatmap_settings: (?String) -> Config::HeatmapSettings
|
|
58
|
+
def editor_args: (?String) -> String?
|
|
59
|
+
def visual_settings: (?String) -> Config::VisualSettings
|
|
60
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Builds and runs an editor command for a logbook file.
|
|
2
|
+
class Mdlogbook::EditorLauncher
|
|
3
|
+
class NoEditorError < Mdlogbook::Error
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def initialize: (file: String, ?today: Date, ?args_template: String?) -> void
|
|
7
|
+
def build_command: () -> Array[String]
|
|
8
|
+
def launch: () -> bool
|
|
9
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Immutable execution context shared by Runner and Executor layers.
|
|
2
|
+
class Mdlogbook::Env
|
|
3
|
+
attr_reader opts: Mdlogbook::CLI::Options
|
|
4
|
+
attr_reader argv: Array[String]
|
|
5
|
+
attr_reader stdout: IO
|
|
6
|
+
attr_reader stderr: IO
|
|
7
|
+
attr_reader today: Date
|
|
8
|
+
attr_reader config: Mdlogbook::Config
|
|
9
|
+
|
|
10
|
+
def initialize: (opts: Mdlogbook::CLI::Options, argv: Array[String], stdout: IO, stderr: IO, today: Date, config: Mdlogbook::Config) -> void
|
|
11
|
+
|
|
12
|
+
def raw_format?: () -> bool
|
|
13
|
+
def grep_format?: () -> bool
|
|
14
|
+
def line_chart_format?: () -> bool
|
|
15
|
+
def grid_format?: () -> bool
|
|
16
|
+
def heatmap_format?: () -> bool
|
|
17
|
+
def overlap_reporting_format?: () -> bool
|
|
18
|
+
def warning_reporting_format?: () -> bool
|
|
19
|
+
def anonymize_format?: () -> bool
|
|
20
|
+
def extract_format?: () -> bool
|
|
21
|
+
def visual_format?: () -> bool
|
|
22
|
+
|
|
23
|
+
def anonymize_settings: () -> Mdlogbook::Config::AnonymizeSettings?
|
|
24
|
+
|
|
25
|
+
def edit?: () -> bool
|
|
26
|
+
def debug?: () -> bool
|
|
27
|
+
def tty?: () -> bool
|
|
28
|
+
|
|
29
|
+
def puts: (*untyped args) -> void
|
|
30
|
+
def warn: (*untyped args) -> void
|
|
31
|
+
def alert: (*untyped args) -> void
|
|
32
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Resolves logbook file paths from config and a year/month pair.
|
|
2
|
+
class Mdlogbook::FileResolver
|
|
3
|
+
attr_reader basedir: String
|
|
4
|
+
attr_reader project: String
|
|
5
|
+
|
|
6
|
+
def initialize: (?config: Mdlogbook::Config, ?basedir: String?) -> void
|
|
7
|
+
def resolve: (Integer, Integer) -> String
|
|
8
|
+
def list_available_months: () -> Array[[Integer, Integer]]
|
|
9
|
+
end
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Formats logbook entries as anonymized markdown for AI analysis.
|
|
2
|
+
class Mdlogbook::Formatter::Anonymize < Mdlogbook::Formatter::Base
|
|
3
|
+
def format_output: (Mdlogbook::Parser::Result, Mdlogbook::WorkThresholds, ?IO, ?anonymize_settings: Mdlogbook::Config::AnonymizeSettings?, ?range_begin: Date?, ?range_end: Date?, **untyped) -> void
|
|
4
|
+
end
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Outputs a reverse-lookup dictionary for anonymized job names.
|
|
2
|
+
class Mdlogbook::Formatter::AnonymizeDict < Mdlogbook::Formatter::Base
|
|
3
|
+
def format_output: (Mdlogbook::Parser::Result, Mdlogbook::WorkThresholds, ?IO, ?anonymize_settings: Mdlogbook::Config::AnonymizeSettings?, **untyped) -> void
|
|
4
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Abstract base class for output formatters.
|
|
2
|
+
class Mdlogbook::Formatter::Base
|
|
3
|
+
PLAIN_SIGNS: Hash[Symbol, String]
|
|
4
|
+
|
|
5
|
+
attr_reader color_reset: String
|
|
6
|
+
attr_reader row_colors: Array[String]
|
|
7
|
+
attr_reader calendar_signs: Hash[Symbol, String]
|
|
8
|
+
attr_reader calendar_colors: Hash[Symbol, String]
|
|
9
|
+
|
|
10
|
+
def initialize: (?color: bool) -> void
|
|
11
|
+
def format_output: (Mdlogbook::Parser::Result, Mdlogbook::WorkThresholds, IO, **untyped) -> void
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
def passthrough?: (String, Array[String]) -> bool
|
|
15
|
+
end
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Formats logbook entries as a weekly calendar grid with daily totals and ratings.
|
|
2
|
+
class Mdlogbook::Formatter::Calendar < Mdlogbook::Formatter::Base
|
|
3
|
+
def format_output: (Mdlogbook::Parser::Result, Mdlogbook::WorkThresholds, ?IO, ?today: Date, ?debug: bool, **untyped) -> void
|
|
4
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Shared methods for building cumulative chart series.
|
|
2
|
+
module Mdlogbook::Formatter::ChartSeriesBuilder
|
|
3
|
+
private
|
|
4
|
+
|
|
5
|
+
def build_actual: (Mdlogbook::Parser::Result, Date, Integer) -> Array[Float]
|
|
6
|
+
def build_linear_target: (Numeric, Integer) -> Array[Float]
|
|
7
|
+
def build_weekday_target: (Mdlogbook::WorkThresholds, Date, Integer) -> Array[Float]
|
|
8
|
+
def working_day?: (Date) -> bool
|
|
9
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Renders multiple months as a grid of line charts.
|
|
2
|
+
class Mdlogbook::Formatter::Grid
|
|
3
|
+
MIN_CHART_WIDTH: Integer
|
|
4
|
+
OVERHEAD: Integer
|
|
5
|
+
GAP: Integer
|
|
6
|
+
|
|
7
|
+
def format_output: (Array[[Mdlogbook::PeriodSpec, Mdlogbook::Parser::Result]], Mdlogbook::WorkThresholds, ?IO, ?today: Date, ?line_chart_config: Mdlogbook::Config::VisualSettings?) -> void
|
|
8
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Renders a day-of-week x hour-of-day density plot showing work patterns.
|
|
2
|
+
class Mdlogbook::Formatter::Heatmap
|
|
3
|
+
SAMPLE_INTERVAL: Integer
|
|
4
|
+
HEIGHT: Integer
|
|
5
|
+
RESOLUTIONS: Hash[String, Integer]
|
|
6
|
+
DEFAULT_RESOLUTION: String
|
|
7
|
+
DAY_LABELS: Array[String]
|
|
8
|
+
|
|
9
|
+
def format_output: (Mdlogbook::Parser::Result, ?IO, ?resolution: String?, ?heatmap_config: Mdlogbook::Config::HeatmapSettings, ?range_begin: Date?, ?range_end: Date?) -> void
|
|
10
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Renders a cumulative-hours line chart for a single calendar month.
|
|
2
|
+
class Mdlogbook::Formatter::LineChart
|
|
3
|
+
SIZES: Hash[String, {width: Integer, height: Integer}?]
|
|
4
|
+
SIZE_ALIASES: Hash[String, String]
|
|
5
|
+
|
|
6
|
+
def format_output: (Mdlogbook::Parser::Result, Mdlogbook::WorkThresholds, ?IO, spec: Mdlogbook::PeriodSpec, ?size: String, ?today: Date, ?line_chart_config: Mdlogbook::Config::VisualSettings?, **untyped) -> void
|
|
7
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Describes a single CLI option with metadata for OptionParser and shell completion.
|
|
2
|
+
class Mdlogbook::OptionDef
|
|
3
|
+
attr_reader short: String?
|
|
4
|
+
attr_reader long: String
|
|
5
|
+
attr_reader arg_name: String?
|
|
6
|
+
attr_reader arg_optional: bool
|
|
7
|
+
attr_reader description: String
|
|
8
|
+
attr_reader type: Class?
|
|
9
|
+
attr_reader values: Array[String]?
|
|
10
|
+
attr_reader completion: (Symbol | String)?
|
|
11
|
+
|
|
12
|
+
def initialize: (
|
|
13
|
+
long: String,
|
|
14
|
+
description: String,
|
|
15
|
+
?short: String?,
|
|
16
|
+
?arg_name: String?,
|
|
17
|
+
?arg_optional: bool,
|
|
18
|
+
?type: Class?,
|
|
19
|
+
?values: Array[String]?,
|
|
20
|
+
?completion: (Symbol | String)?
|
|
21
|
+
) -> void
|
|
22
|
+
|
|
23
|
+
def flag?: () -> bool
|
|
24
|
+
def long_name: () -> String
|
|
25
|
+
def optparse_args: () -> Array[untyped]
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def optparse_long_flag: () -> String
|
|
30
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Collects CLI option definitions and generates OptionParser instances and shell completion scripts.
|
|
2
|
+
class Mdlogbook::OptionRegistry
|
|
3
|
+
def initialize: () -> void
|
|
4
|
+
def separator: (String text) -> void
|
|
5
|
+
def option: (**untyped kwargs) ?{ (untyped) -> void } -> Mdlogbook::OptionDef
|
|
6
|
+
def definitions: () -> Array[Mdlogbook::OptionDef]
|
|
7
|
+
def to_option_parser: (banner: String) -> OptionParser
|
|
8
|
+
def completion_bash: () -> String
|
|
9
|
+
def completion_zsh: () -> String
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def bash_compgen: (Mdlogbook::OptionDef d) -> String
|
|
14
|
+
def zsh_specs: (Mdlogbook::OptionDef d) -> Array[String]
|
|
15
|
+
def zsh_escape_desc: (String desc) -> String
|
|
16
|
+
def zsh_arg_part: (Mdlogbook::OptionDef d) -> String
|
|
17
|
+
def zsh_action: (Mdlogbook::OptionDef d) -> String
|
|
18
|
+
end
|