time_buffer 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fb5a16f7fd58ccfb39b6d84410ec6c4e1326f119024189f652674dacf590170f
4
+ data.tar.gz: 5cc84eaaff62ae9741a9008e4ed5b8193d8f40f8d8e110c1d2b40c8f87152e08
5
+ SHA512:
6
+ metadata.gz: 560ef5753cb3eb51bd1464df41dfa3d230a2f4676919af9d3ebb737d69c267bcd60adaeecf0b1477771fd5c34dc3f984c69592e8958bed8f42b51fc09ecb3dd8
7
+ data.tar.gz: '003745089e2980687ca331cebe91a3447c28d4e2262535a959c3fbc5a11d383ecac988e249a4da1634d7a0279a2088cf815ec270e18ffc70e902fec0a4fc47b6'
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.solargraph.yml ADDED
@@ -0,0 +1 @@
1
+ reporters: []
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/standardrb/standard
3
+ ruby_version: 3.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-08-10
4
+
5
+ - Initial release
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # TimeBuffer
2
+
3
+ A gem that tracks your time.
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ $ bundle add time_buffer
10
+
11
+ If bundler is not being used to manage dependencies, install the gem by executing:
12
+
13
+ $ gem install time_buffer
14
+
15
+ ## Usage
16
+
17
+ In IRB:
18
+
19
+ ```rb
20
+ require "time_buffer"
21
+
22
+ TimeBuffer::Tracker.start
23
+ ```
24
+
25
+ This will initialize the SQLite database and start the tracker.
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/StevenElberger/time_buffer.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TimeBuffer
4
+ class ApplicationHandler
5
+ def initialize(app_data)
6
+ @app_data = app_data
7
+ end
8
+
9
+ def insert_application
10
+ TimeBuffer::DatabaseConnector.new.execute(
11
+ "INSERT OR IGNORE INTO applications (bundle_id, app_name) VALUES (?, ?)",
12
+ [app_data.bundle_id, app_data.name]
13
+ )
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :app_data
19
+ end
20
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TimeBuffer
4
+ class DatabaseConnector
5
+ FILE_LOCATION = "usage_data.db"
6
+
7
+ def initialize
8
+ return if File.exist?(FILE_LOCATION)
9
+
10
+ # 1. Applications Table
11
+ connection.execute <<-SQL
12
+ CREATE TABLE IF NOT EXISTS applications (
13
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
14
+ bundle_id TEXT UNIQUE NOT NULL,
15
+ app_name TEXT NOT NULL
16
+ );
17
+ SQL
18
+
19
+ # 2. Time Sessions Table
20
+ connection.execute <<-SQL
21
+ CREATE TABLE IF NOT EXISTS time_sessions (
22
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
23
+ application_id INTEGER NOT NULL,
24
+ start_time DATETIME NOT NULL,
25
+ end_time DATETIME,
26
+ metadata JSONB,
27
+ FOREIGN KEY (application_id) REFERENCES applications(id)
28
+ );
29
+ SQL
30
+
31
+ # 3. Daily Summaries Table
32
+ connection.execute <<-SQL
33
+ CREATE TABLE IF NOT EXISTS daily_summaries (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ application_id INTEGER NOT NULL,
36
+ date DATE NOT NULL,
37
+ total_duration INTEGER NOT NULL,
38
+ FOREIGN KEY (application_id) REFERENCES applications(id),
39
+ UNIQUE(application_id, date)
40
+ );
41
+ SQL
42
+
43
+ puts "Database initialized successfully."
44
+ end
45
+
46
+ def connect
47
+ @db = SQLite3::Database.new(FILE_LOCATION)
48
+ end
49
+
50
+ def connection
51
+ @db || connect
52
+ end
53
+
54
+ def execute(sql, *params)
55
+ # puts "Writing to db: #{params}"
56
+ connection.execute(sql, *params)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TimeBuffer
4
+ class OsaScript
5
+ class << self
6
+ def app_data
7
+ current_app_bundle_id = `osascript -e 'id of application (path to frontmost application as text)'`.strip
8
+ app = OsaScriptAppBuilder.build(current_app_bundle_id)
9
+ puts "app name: #{app.name} | bundle id: #{app.bundle_id} | class: #{app.class}"
10
+ app
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TimeBuffer
4
+ class OsaScriptApp
5
+ attr_reader :bundle_id
6
+
7
+ def initialize(app_bundle_id)
8
+ @bundle_id = app_bundle_id
9
+ end
10
+
11
+ def name
12
+ @name ||= `osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true'`.strip
13
+ end
14
+
15
+ def metadata = {}
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "osa_script_app_builder"
4
+
5
+ module TimeBuffer
6
+ class OsaScriptAppBrowser < OsaScriptApp
7
+ KNOWN_BROWSERS = %w[com.google.Chrome]
8
+
9
+ def self.register_known_browsers
10
+ KNOWN_BROWSERS.each do |bundle_id|
11
+ OsaScriptAppBuilder.register(app_bundle_id: bundle_id, app_class: self)
12
+ end
13
+ end
14
+
15
+ register_known_browsers
16
+
17
+ def initialize(app_bundle_id)
18
+ @bundle_id = app_bundle_id
19
+ end
20
+
21
+ def metadata
22
+ {
23
+ tab_title: tab_title
24
+ }
25
+ end
26
+
27
+ def tab_title
28
+ `osascript -e 'tell application "Google Chrome" to get title of active tab of front window'`.strip
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TimeBuffer
4
+ class OsaScriptAppBuilder
5
+ class << self
6
+ REGISTRY = {}
7
+
8
+ def register(app_bundle_id:, app_class:)
9
+ REGISTRY[app_bundle_id] = app_class
10
+ end
11
+
12
+ def build(app_bundle_id)
13
+ app_class = REGISTRY[app_bundle_id]
14
+
15
+ if app_class.nil?
16
+ OsaScriptApp.new(app_bundle_id)
17
+ else
18
+ app_class.new(app_bundle_id)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module TimeBuffer
6
+ class SessionHandler
7
+ def initialize(start_time:, end_time:)
8
+ @start_time = start_time
9
+ @end_time = end_time
10
+ end
11
+
12
+ def insert_time_session(app_data)
13
+ application_id = TimeBuffer::DatabaseConnector.new.execute(
14
+ "SELECT id FROM applications WHERE bundle_id = ?",
15
+ [app_data.bundle_id]
16
+ ).first&.first
17
+
18
+ puts "application ID: #{application_id}"
19
+
20
+ TimeBuffer::DatabaseConnector.new.execute(
21
+ "INSERT INTO time_sessions (application_id, start_time, end_time, metadata) VALUES (?, ?, ?, ?);",
22
+ [application_id, start_time.strftime("%Y-%m-%d %H:%M:%S"), end_time.strftime("%Y-%m-%d %H:%M:%S"), app_data.metadata.to_json]
23
+ )
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :start_time, :end_time
29
+ end
30
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TimeBuffer
4
+ class StateDetector
5
+ attr_reader :current_app_data, :last_interaction_at, :previous_app, :previous_metadata
6
+
7
+ def initialize
8
+ @previous_app = nil
9
+ @previous_metadata = {}
10
+ @last_interaction_at = Time.now
11
+ @current_app_data = nil
12
+ end
13
+
14
+ def app_changed?
15
+ @current_app_data = OsaScript.app_data
16
+ @current_app_data.name != @previous_app
17
+ end
18
+
19
+ def metadata_changed?
20
+ metadata = current_app_data.metadata
21
+ metadata && metadata != previous_metadata && current_app_data.name == previous_app
22
+ end
23
+
24
+ def update_last_interaction_time(now)
25
+ @last_interaction_at = now
26
+ update_state
27
+ end
28
+
29
+ private
30
+
31
+ def update_state
32
+ @previous_app = current_app_data.name
33
+ @previous_metadata = current_app_data.metadata
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TimeBuffer
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "time_buffer/application_handler"
4
+ require_relative "time_buffer/database_connector"
5
+ require_relative "time_buffer/osa_script"
6
+ require_relative "time_buffer/osa_script_app"
7
+ require_relative "time_buffer/osa_script_app_browser"
8
+ require_relative "time_buffer/osa_script_app_builder"
9
+ require_relative "time_buffer/session_handler"
10
+ require_relative "time_buffer/state_detector"
11
+ require_relative "time_buffer/version"
12
+ require "sqlite3"
13
+ module TimeBuffer
14
+ class Tracker
15
+ def initialize
16
+ @state_detector = StateDetector.new
17
+ end
18
+
19
+ def start
20
+ loop do
21
+ now = Time.now
22
+
23
+ if state_detector.app_changed?
24
+ handle_app_change(state_detector.current_app_data, now)
25
+ elsif state_detector.metadata_changed?
26
+ handle_metadata_change(state_detector.current_app_data, now)
27
+ end
28
+
29
+ sleep 1
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :state_detector
36
+
37
+ def handle_app_change(app_data, now)
38
+ ApplicationHandler.new(app_data).insert_application
39
+ SessionHandler.new(start_time: state_detector.last_interaction_at, end_time: now).insert_time_session(app_data)
40
+ state_detector.update_last_interaction_time(now)
41
+ end
42
+
43
+ def handle_metadata_change(app_data, now)
44
+ SessionHandler.new(start_time: state_detector.last_interaction_at, end_time: now).insert_time_session(app_data)
45
+ state_detector.update_last_interaction_time(now)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,4 @@
1
+ module TimeBuffer
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: time_buffer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Steven Elberger
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-08-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sqlite3
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ description:
28
+ email:
29
+ - stevenelberger@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".rspec"
35
+ - ".solargraph.yml"
36
+ - ".standard.yml"
37
+ - CHANGELOG.md
38
+ - README.md
39
+ - Rakefile
40
+ - lib/time_buffer.rb
41
+ - lib/time_buffer/application_handler.rb
42
+ - lib/time_buffer/database_connector.rb
43
+ - lib/time_buffer/osa_script.rb
44
+ - lib/time_buffer/osa_script_app.rb
45
+ - lib/time_buffer/osa_script_app_browser.rb
46
+ - lib/time_buffer/osa_script_app_builder.rb
47
+ - lib/time_buffer/session_handler.rb
48
+ - lib/time_buffer/state_detector.rb
49
+ - lib/time_buffer/version.rb
50
+ - sig/time_buffer.rbs
51
+ homepage: https://github.com/StevenElberger/time_buffer
52
+ licenses: []
53
+ metadata:
54
+ allowed_push_host: https://rubygems.org
55
+ homepage_uri: https://github.com/StevenElberger/time_buffer
56
+ source_code_uri: https://github.com/StevenElberger/time_buffer
57
+ changelog_uri: https://github.com/StevenElberger/time_buffer/blob/main/CHANGELOG.md
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 3.0.0
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 3.5.17
74
+ signing_key:
75
+ specification_version: 4
76
+ summary: Tracks time spent on all applications
77
+ test_files: []