rewind-timeline 0.1.0__py3-none-any.whl

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.
collectors/__init__.py ADDED
File without changes
collectors/collect.py ADDED
@@ -0,0 +1,52 @@
1
+ # collect.py
2
+
3
+ import time
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ sys.path.append(
8
+ str(Path(__file__).resolve().parent.parent)
9
+ )
10
+
11
+ from database import Database
12
+
13
+ from collectors.packages import PackageCollector
14
+ from collectors.services import ServiceCollector
15
+ from collectors.performance import PerformanceCollector
16
+ from collectors.shell import ShellCollector
17
+ from collectors.files import FileCollector
18
+
19
+ def start(stop_event=None):
20
+ db = Database()
21
+
22
+ packages = PackageCollector()
23
+ services = ServiceCollector()
24
+ performance = PerformanceCollector()
25
+ shell = ShellCollector()
26
+
27
+ # Start file monitoring (queue-based) in parallel with other collectors.
28
+ # Watching / can be noisy; change watch_root if needed.
29
+ file_collector = FileCollector(watch_path="/", recursive=True)
30
+ file_collector.start()
31
+
32
+
33
+ print("Rewind monitor started.")
34
+
35
+ while True:
36
+ if stop_event is not None and stop_event.is_set():
37
+ break
38
+
39
+ all_events = []
40
+
41
+ all_events.extend(packages.read_new_events())
42
+ all_events.extend(services.check_changes())
43
+ all_events.extend(performance.check())
44
+ all_events.extend(shell.read_new_commands())
45
+ all_events.extend(file_collector.read_new_events())
46
+
47
+ db.add_events(all_events)
48
+
49
+ time.sleep(10)
50
+
51
+ file_collector.stop()
52
+ db.close()
collectors/files.py ADDED
@@ -0,0 +1,104 @@
1
+ import os
2
+ import time
3
+ import threading
4
+ from pathlib import Path
5
+ from queue import Queue, Empty
6
+
7
+ from watchdog.observers import Observer
8
+ from watchdog.events import FileSystemEventHandler
9
+
10
+
11
+ class FileCollector(FileSystemEventHandler):
12
+ """Queue-based file change collector.
13
+
14
+ Produces events into an internal Queue so the main loop can batch them.
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ watch_path: str | Path = "/",
20
+ recursive: bool = True,
21
+ throttle_seconds: float = 2.0,
22
+ ):
23
+ super().__init__()
24
+ self.watch_path = Path(watch_path)
25
+ self.recursive = recursive
26
+
27
+ self._queue: Queue = Queue()
28
+
29
+ # Debounce by (event_type, src_path) and only emit once per throttle window.
30
+ self.throttle_seconds = throttle_seconds
31
+ self._last_emitted = {}
32
+
33
+ self._observer: Observer | None = None
34
+
35
+ def start(self):
36
+ if self._observer is not None:
37
+ return
38
+
39
+ observer = Observer()
40
+ observer.schedule(self, str(self.watch_path), recursive=self.recursive)
41
+ observer.start()
42
+ self._observer = observer
43
+
44
+ def stop(self):
45
+ if self._observer is None:
46
+ return
47
+ self._observer.stop()
48
+ self._observer.join(timeout=5)
49
+ self._observer = None
50
+
51
+ def _enqueue(self, event_type: str, src_path: str):
52
+ now = time.time()
53
+ key = (event_type, src_path)
54
+ last = self._last_emitted.get(key)
55
+ if last is not None and (now - last) < self.throttle_seconds:
56
+ return
57
+
58
+ self._last_emitted[key] = now
59
+ title = f"{event_type}: {src_path}"
60
+
61
+ # Keep it consistent with other collectors.
62
+ self._queue.put({
63
+ "category": "file",
64
+ "title": title,
65
+ })
66
+
67
+ def on_modified(self, event):
68
+ if event.is_directory:
69
+ return
70
+ self._enqueue("Modified", event.src_path)
71
+
72
+ def on_created(self, event):
73
+ if event.is_directory:
74
+ return
75
+ self._enqueue("Created", event.src_path)
76
+
77
+ def on_deleted(self, event):
78
+ if event.is_directory:
79
+ return
80
+ self._enqueue("Deleted", event.src_path)
81
+
82
+ def read_new_events(self, max_events: int = 500):
83
+ events = []
84
+ for _ in range(max_events):
85
+ try:
86
+ events.append(self._queue.get_nowait())
87
+ except Empty:
88
+ break
89
+ return events
90
+
91
+
92
+ # Backwards-compatible function kept for older imports.
93
+ # This project primarily uses FileCollector.
94
+
95
+ def monitor(path: str | Path):
96
+ collector = FileCollector(watch_path=path)
97
+ collector.start()
98
+
99
+ try:
100
+ while True:
101
+ time.sleep(1)
102
+ except KeyboardInterrupt:
103
+ collector.stop()
104
+
collectors/packages.py ADDED
@@ -0,0 +1,62 @@
1
+ # collectors/packages.py
2
+ import os
3
+ import re
4
+
5
+ PACMAN_LOG = "/var/log/pacman.log"
6
+
7
+
8
+ class PackageCollector:
9
+ def __init__(self):
10
+ self.last_position = 0
11
+
12
+ def read_new_events(self):
13
+ events = []
14
+
15
+ if not os.path.exists(PACMAN_LOG):
16
+ return events
17
+
18
+ with open(PACMAN_LOG, "r") as file:
19
+ file.seek(self.last_position)
20
+
21
+ for line in file:
22
+ line = line.strip()
23
+
24
+ installed = re.search(
25
+ r"\[(.*?)\].*installed (.*?) \(",
26
+ line
27
+ )
28
+
29
+ removed = re.search(
30
+ r"\[(.*?)\].*removed (.*?) \(",
31
+ line
32
+ )
33
+
34
+ upgraded = re.search(
35
+ r"\[(.*?)\].*upgraded (.*?) \(",
36
+ line
37
+ )
38
+
39
+ if installed:
40
+ events.append({
41
+ "timestamp": installed.group(1),
42
+ "category": "package",
43
+ "title": f"Installed {installed.group(2)}"
44
+ })
45
+
46
+ elif removed:
47
+ events.append({
48
+ "timestamp": removed.group(1),
49
+ "category": "package",
50
+ "title": f"Removed {removed.group(2)}"
51
+ })
52
+
53
+ elif upgraded:
54
+ events.append({
55
+ "timestamp": upgraded.group(1),
56
+ "category": "package",
57
+ "title": f"Upgraded {upgraded.group(2)}"
58
+ })
59
+
60
+ self.last_position = file.tell()
61
+
62
+ return events
@@ -0,0 +1,50 @@
1
+ # collectors/performance.py
2
+
3
+ import psutil
4
+ import time
5
+
6
+
7
+ class PerformanceCollector:
8
+ def __init__(self):
9
+ self.cpu_limit = 90
10
+ self.memory_limit = 90
11
+ self.disk_limit = 90
12
+
13
+ # Prevent alert spam when the system stays above threshold.
14
+ self.cooldown_seconds = 300 # 5 minutes
15
+ self._last_alert_ts = {}
16
+
17
+ # Warm up cpu_percent so subsequent calls don't block.
18
+ try:
19
+ psutil.cpu_percent(interval=None)
20
+ except Exception:
21
+ pass
22
+
23
+ def _emit(self, key, title, now, events):
24
+ last = self._last_alert_ts.get(key)
25
+ if last is None or (now - last) >= self.cooldown_seconds:
26
+ self._last_alert_ts[key] = now
27
+ events.append({
28
+ "category": "performance",
29
+ "title": title,
30
+ })
31
+
32
+ def check(self):
33
+ events = []
34
+ now = time.time()
35
+
36
+ # Non-blocking sample after warmup.
37
+ cpu = psutil.cpu_percent(interval=None)
38
+ memory = psutil.virtual_memory().percent
39
+ disk = psutil.disk_usage("/").percent
40
+
41
+ if cpu >= self.cpu_limit:
42
+ self._emit("cpu", f"CPU usage reached {cpu}%", now, events)
43
+
44
+ if memory >= self.memory_limit:
45
+ self._emit("memory", f"Memory usage reached {memory}%", now, events)
46
+
47
+ if disk >= self.disk_limit:
48
+ self._emit("disk", f"Disk usage reached {disk}%", now, events)
49
+
50
+ return events
collectors/services.py ADDED
@@ -0,0 +1,56 @@
1
+ # collectors/services.py
2
+
3
+ import subprocess
4
+
5
+
6
+ class ServiceCollector:
7
+ def __init__(self):
8
+ self.previous_states = {}
9
+
10
+ def get_services(self):
11
+ result = subprocess.run(
12
+ [
13
+ "systemctl",
14
+ "list-units",
15
+ "--type=service",
16
+ "--no-pager",
17
+ "--no-legend"
18
+ ],
19
+ capture_output=True,
20
+ text=True
21
+ )
22
+
23
+ services = {}
24
+
25
+ for line in result.stdout.splitlines():
26
+ parts = line.split()
27
+
28
+ if len(parts) >= 4:
29
+ name = parts[0]
30
+ active = parts[2]
31
+
32
+ services[name] = active
33
+
34
+ return services
35
+
36
+ def check_changes(self):
37
+ events = []
38
+
39
+ current = self.get_services()
40
+
41
+ for service, state in current.items():
42
+ old_state = self.previous_states.get(service)
43
+
44
+ if old_state is None:
45
+ self.previous_states[service] = state
46
+ continue
47
+
48
+ if old_state != state:
49
+ events.append({
50
+ "category": "service",
51
+ "title": f"{service} changed: {old_state} → {state}"
52
+ })
53
+
54
+ self.previous_states[service] = state
55
+
56
+ return events
collectors/shell.py ADDED
@@ -0,0 +1,55 @@
1
+ from pathlib import Path
2
+ import time
3
+
4
+
5
+ class ShellCollector:
6
+ def __init__(self):
7
+ self.history_file = Path.home() / ".bash_history"
8
+
9
+ # File offset for incremental reads.
10
+ self.position = 0
11
+ self._last_seen_ts = {}
12
+
13
+ # Cooldown to reduce duplicates for the exact same command.
14
+ self.cooldown_seconds = 60
15
+
16
+ def _current_size(self):
17
+ try:
18
+ return self.history_file.stat().st_size
19
+ except FileNotFoundError:
20
+ return 0
21
+
22
+ def read_new_commands(self):
23
+ events = []
24
+
25
+ if not self.history_file.exists():
26
+ return events
27
+
28
+ current_size = self._current_size()
29
+
30
+ # Handle rotation/truncation: if file shrank, reset offset.
31
+ if current_size < self.position:
32
+ self.position = 0
33
+
34
+ now = time.time()
35
+
36
+ with open(self.history_file, "r", encoding="utf-8", errors="ignore") as file:
37
+ file.seek(self.position)
38
+
39
+ for line in file:
40
+ command = line.strip()
41
+ if not command:
42
+ continue
43
+
44
+ last = self._last_seen_ts.get(command)
45
+ if last is None or (now - last) >= self.cooldown_seconds:
46
+ self._last_seen_ts[command] = now
47
+ events.append({
48
+ "category": "shell",
49
+ "title": command,
50
+ })
51
+
52
+ self.position = file.tell()
53
+
54
+ return events
55
+
commands/__init__.py ADDED
File without changes
commands/search.py ADDED
@@ -0,0 +1,28 @@
1
+ # commands/search.py
2
+
3
+ from database import Database
4
+
5
+
6
+ def run(keyword):
7
+ db = Database()
8
+
9
+ results = db.search_events(keyword)
10
+
11
+ print(f"\nResults for: {keyword}\n")
12
+
13
+ if not results:
14
+ print("No matching events found.")
15
+ db.close()
16
+ return
17
+
18
+ for timestamp, category, title, details in results:
19
+ print(
20
+ f"{timestamp} "
21
+ f"[{category.upper()}] "
22
+ f"{title}"
23
+ )
24
+
25
+ if details:
26
+ print(f" {details}")
27
+
28
+ db.close()
commands/stats.py ADDED
@@ -0,0 +1,54 @@
1
+ # commands/stats.py
2
+
3
+ from database import Database
4
+
5
+
6
+ def run():
7
+ db = Database()
8
+
9
+ cursor = db.cursor
10
+
11
+ # Total events
12
+ cursor.execute("""
13
+ SELECT COUNT(*)
14
+ FROM events
15
+ """)
16
+
17
+ total = cursor.fetchone()[0]
18
+
19
+ print("\nRewind Statistics\n")
20
+ print(f"Total events: {total}\n")
21
+
22
+ # Events per category
23
+ print("By category:")
24
+
25
+ cursor.execute("""
26
+ SELECT category, COUNT(*)
27
+ FROM events
28
+ GROUP BY category
29
+ ORDER BY COUNT(*) DESC
30
+ """)
31
+
32
+ rows = cursor.fetchall()
33
+
34
+ for category, count in rows:
35
+ print(f"{category.upper():15} {count}")
36
+
37
+ # Most active day
38
+ cursor.execute("""
39
+ SELECT DATE(timestamp), COUNT(*)
40
+ FROM events
41
+ GROUP BY DATE(timestamp)
42
+ ORDER BY COUNT(*) DESC
43
+ LIMIT 1
44
+ """)
45
+
46
+ result = cursor.fetchone()
47
+
48
+ if result:
49
+ date, count = result
50
+
51
+ print("\nMost active day:")
52
+ print(f"{date} ({count} events)")
53
+
54
+ db.close()
commands/today.py ADDED
@@ -0,0 +1,31 @@
1
+ # commands/today.py
2
+ from datetime import datetime
3
+ from database import Database
4
+
5
+
6
+ def run():
7
+ db = Database()
8
+
9
+ today = datetime.now().strftime("%Y-%m-%d")
10
+
11
+ events = db.get_events_by_date(today)
12
+
13
+ print(f"\nRewind - {today}\n")
14
+
15
+ if not events:
16
+ print("No events found.")
17
+ return
18
+
19
+ for timestamp, category, title, details in events:
20
+ time_only = timestamp.split()[1]
21
+
22
+ print(
23
+ f"[{time_only}] "
24
+ f"[{category.upper()}] "
25
+ f"{title}"
26
+ )
27
+
28
+ if details:
29
+ print(f" {details}")
30
+
31
+ db.close()
commands/yesterday.py ADDED
@@ -0,0 +1,35 @@
1
+ # commands/yesterday.py
2
+
3
+ from datetime import datetime, timedelta
4
+ from database import Database
5
+
6
+
7
+ def run():
8
+ db = Database()
9
+
10
+ yesterday = (
11
+ datetime.now() - timedelta(days=1)
12
+ ).strftime("%Y-%m-%d")
13
+
14
+ events = db.get_events_by_date(yesterday)
15
+
16
+ print(f"\nRewind - {yesterday}\n")
17
+
18
+ if not events:
19
+ print("No events found.")
20
+ db.close()
21
+ return
22
+
23
+ for timestamp, category, title, details in events:
24
+ time_only = timestamp.split()[1]
25
+
26
+ print(
27
+ f"[{time_only}] "
28
+ f"[{category.upper()}] "
29
+ f"{title}"
30
+ )
31
+
32
+ if details:
33
+ print(f" {details}")
34
+
35
+ db.close()
database.py ADDED
@@ -0,0 +1,104 @@
1
+ import sqlite3
2
+ from pathlib import Path
3
+ from datetime import datetime
4
+
5
+ DB_PATH = Path.home() / ".rewind.db"
6
+
7
+
8
+ class Database:
9
+ def __init__(self):
10
+ self.conn = sqlite3.connect(DB_PATH)
11
+ self.cursor = self.conn.cursor()
12
+ self.create_tables()
13
+
14
+ def create_tables(self):
15
+ self.cursor.execute("""
16
+ CREATE TABLE IF NOT EXISTS events (
17
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
18
+ timestamp TEXT NOT NULL,
19
+ category TEXT NOT NULL,
20
+ title TEXT NOT NULL,
21
+ details TEXT
22
+ )
23
+ """)
24
+
25
+ # Indexes for faster timeline queries as the DB grows.
26
+ self.cursor.execute(
27
+ "CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp)"
28
+ )
29
+ self.cursor.execute(
30
+ "CREATE INDEX IF NOT EXISTS idx_events_category ON events(category)"
31
+ )
32
+ self.cursor.execute(
33
+ "CREATE INDEX IF NOT EXISTS idx_events_title ON events(title)"
34
+ )
35
+
36
+ self.conn.commit()
37
+
38
+ def add_event(self, category, title, details="", timestamp=None):
39
+ if timestamp is None:
40
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
41
+ self.cursor.execute(
42
+ "INSERT INTO events (timestamp, category, title, details) VALUES (?, ?, ?, ?)",
43
+ (timestamp, category, title, details),
44
+ )
45
+ self.conn.commit()
46
+
47
+ def add_events(self, events):
48
+ """Bulk insert events.
49
+
50
+ events: iterable of dicts with keys: category, title
51
+ optional keys: details, timestamp
52
+ """
53
+ rows = []
54
+ for event in events:
55
+ category = event.get("category")
56
+ title = event.get("title")
57
+ details = event.get("details", "")
58
+ timestamp = event.get("timestamp")
59
+ if category is None or title is None:
60
+ continue
61
+ if timestamp is None:
62
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
63
+ rows.append((timestamp, category, title, details))
64
+
65
+ if not rows:
66
+ return
67
+
68
+ self.cursor.executemany(
69
+ "INSERT INTO events (timestamp, category, title, details) VALUES (?, ?, ?, ?)",
70
+ rows,
71
+ )
72
+ self.conn.commit()
73
+
74
+ def get_all_events(self):
75
+ self.cursor.execute("""
76
+ SELECT id, timestamp, category, title, details
77
+ FROM events
78
+ ORDER BY timestamp DESC
79
+ """)
80
+
81
+ return self.cursor.fetchall()
82
+
83
+ def get_events_by_date(self, date):
84
+ self.cursor.execute("""
85
+ SELECT timestamp, category, title, details
86
+ FROM events
87
+ WHERE DATE(timestamp) = ?
88
+ ORDER BY timestamp
89
+ """, (date,))
90
+
91
+ return self.cursor.fetchall()
92
+
93
+ def search_events(self, keyword):
94
+ self.cursor.execute("""
95
+ SELECT timestamp, category, title, details
96
+ FROM events
97
+ WHERE title LIKE ? OR details LIKE ?
98
+ ORDER BY timestamp DESC
99
+ """, (f"%{keyword}%", f"%{keyword}%"))
100
+
101
+ return self.cursor.fetchall()
102
+
103
+ def close(self):
104
+ self.conn.close()
rewind.py ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import sys
4
+ import threading
5
+ import signal
6
+
7
+ from commands import today
8
+ from commands import yesterday
9
+ from commands import search
10
+ from commands import stats
11
+ from collectors import collect
12
+
13
+
14
+
15
+ def show_help():
16
+ print("""
17
+ Rewind - Linux Time Machine
18
+
19
+ Usage:
20
+ rewind today
21
+ rewind yesterday
22
+ rewind search <keyword>
23
+ rewind stats
24
+ rewind help
25
+ """)
26
+
27
+
28
+ def main():
29
+ stop_event = threading.Event()
30
+
31
+ def start_collector():
32
+ collect.start(stop_event=stop_event)
33
+
34
+ collector_thread = threading.Thread(target=start_collector, daemon=True)
35
+
36
+ def _handle_signal(signum, frame):
37
+ stop_event.set()
38
+ raise SystemExit(0)
39
+
40
+ signal.signal(signal.SIGINT, _handle_signal)
41
+ signal.signal(signal.SIGTERM, _handle_signal)
42
+
43
+ collector_thread.start()
44
+
45
+ if len(sys.argv) < 2:
46
+ show_help()
47
+ return
48
+
49
+ command = sys.argv[1]
50
+
51
+ if command == "monitor":
52
+ collect.start()
53
+
54
+ elif command == "today":
55
+ today.run()
56
+
57
+ elif command == "yesterday":
58
+ yesterday.run()
59
+
60
+ elif command == "search":
61
+ if len(sys.argv) < 3:
62
+ print("Error: missing search keyword.")
63
+ return
64
+
65
+ keyword = " ".join(sys.argv[2:])
66
+ search.run(keyword)
67
+
68
+ elif command == "stats":
69
+ stats.run()
70
+
71
+ elif command == "help":
72
+ show_help()
73
+
74
+ else:
75
+ print(f"Unknown command: {command}")
76
+ show_help()
77
+
78
+
79
+ if __name__ == "__main__":
80
+ main()
@@ -0,0 +1,274 @@
1
+ Metadata-Version: 2.4
2
+ Name: rewind-timeline
3
+ Version: 0.1.0
4
+ Summary: Linux timeline collector and monitoring tool
5
+ Home-page: https://github.com/LaVenganzaDelLadron/rewind.git
6
+ Author: DarkGlitch
7
+ Author-email: darkglitch5417@gmail.com
8
+ License: MIT
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Dynamic: license-file
13
+
14
+ # Rewind
15
+
16
+ **Rewind** is a Linux Time Machine that records important system events and allows users to revisit what happened on their machine.
17
+
18
+ It helps answer questions such as:
19
+
20
+ * What changed yesterday?
21
+ * Why did my system become slow?
22
+ * Which package was installed?
23
+ * Which service stopped?
24
+ * What commands did I run?
25
+ * Which files were modified?
26
+
27
+ Rewind stores these events locally in a SQLite database and provides a simple command-line interface for exploring the system timeline.
28
+
29
+ ---
30
+
31
+ ## Features
32
+
33
+ * Package monitoring (Pacman support)
34
+ * Service monitoring
35
+ * Performance monitoring
36
+ * Shell history tracking
37
+ * File change monitoring
38
+ * Timeline search
39
+ * Daily activity reports
40
+ * System statistics
41
+
42
+ ---
43
+
44
+ ## Project Structure
45
+
46
+ ```text
47
+ rewind/
48
+ ├── collect.py
49
+ ├── rewind.py
50
+ ├── database.py
51
+
52
+ ├── collectors/
53
+ │ ├── files.py
54
+ │ ├── packages.py
55
+ │ ├── performance.py
56
+ │ ├── services.py
57
+ │ └── shell.py
58
+
59
+ ├── commands/
60
+ │ ├── search.py
61
+ │ ├── stats.py
62
+ │ ├── today.py
63
+ │ └── yesterday.py
64
+
65
+ └── rewind.db
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Requirements
71
+
72
+ * Python 3.10+
73
+ * Linux
74
+ * systemd
75
+ * SQLite
76
+
77
+ Install dependencies:
78
+
79
+ ```bash
80
+ pip install psutil watchdog
81
+ ```
82
+
83
+ ---
84
+
85
+ ## Running the Collector
86
+
87
+ The collector continuously monitors the system and stores events.
88
+
89
+ ```bash
90
+ python collect.py
91
+ ```
92
+
93
+ The collector records:
94
+
95
+ * package changes
96
+ * service state changes
97
+ * performance alerts
98
+ * shell history
99
+ * file modifications
100
+
101
+ ---
102
+
103
+ ## Commands
104
+
105
+ ### View today's events
106
+
107
+ ```bash
108
+ python rewind.py today
109
+ ```
110
+
111
+ ### View yesterday's events
112
+
113
+ ```bash
114
+ python rewind.py yesterday
115
+ ```
116
+
117
+ ### Search the timeline
118
+
119
+ ```bash
120
+ python rewind.py search nginx
121
+ ```
122
+
123
+ ### View statistics
124
+
125
+ ```bash
126
+ python rewind.py stats
127
+ ```
128
+
129
+ ### Show help
130
+
131
+ ```bash
132
+ python rewind.py help
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Example
138
+
139
+ ```text
140
+ $ python rewind.py today
141
+
142
+ Rewind - 2026-06-27
143
+
144
+ [08:32:10] [PACKAGE] Installed nginx
145
+ [09:15:22] [FILE] Modified /etc/ssh/sshd_config
146
+ [11:10:33] [PERFORMANCE] CPU usage reached 95%
147
+ [13:00:17] [SERVICE] nginx.service restarted
148
+ [13:01:55] [SHELL] sudo systemctl restart nginx
149
+ ```
150
+
151
+ ---
152
+
153
+ ## Database
154
+
155
+ Rewind stores events inside a local SQLite database.
156
+
157
+ Location:
158
+
159
+ ```text
160
+ ~/.rewind.db
161
+ ```
162
+
163
+ Schema:
164
+
165
+ ```sql
166
+ CREATE TABLE events (
167
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
168
+ timestamp TEXT NOT NULL,
169
+ category TEXT NOT NULL,
170
+ title TEXT NOT NULL,
171
+ details TEXT
172
+ );
173
+ ```
174
+
175
+ ---
176
+
177
+ ## Current Collectors
178
+
179
+ ### Package Collector
180
+
181
+ Parses:
182
+
183
+ ```text
184
+ /var/log/pacman.log
185
+ ```
186
+
187
+ Tracks:
188
+
189
+ * installed packages
190
+ * removed packages
191
+ * upgraded packages
192
+
193
+ ---
194
+
195
+ ### Service Collector
196
+
197
+ Uses:
198
+
199
+ ```text
200
+ systemctl
201
+ ```
202
+
203
+ Tracks:
204
+
205
+ * started services
206
+ * stopped services
207
+ * restarted services
208
+ * failed services
209
+
210
+ ---
211
+
212
+ ### Performance Collector
213
+
214
+ Monitors:
215
+
216
+ * CPU usage
217
+ * memory usage
218
+ * disk usage
219
+
220
+ ---
221
+
222
+ ### Shell Collector
223
+
224
+ Reads:
225
+
226
+ ```text
227
+ ~/.bash_history
228
+ ```
229
+
230
+ Tracks executed commands.
231
+
232
+ ---
233
+
234
+ ### File Collector
235
+
236
+ Uses:
237
+
238
+ ```text
239
+ watchdog
240
+ ```
241
+
242
+ Tracks:
243
+
244
+ * created files
245
+ * modified files
246
+ * deleted files
247
+
248
+ ---
249
+
250
+ ## Roadmap
251
+
252
+ * systemd service support
253
+ * daemon mode
254
+ * multi-distribution package support
255
+ * export reports
256
+ * weekly summaries
257
+ * interactive TUI
258
+ * notifications
259
+ * command learning mode
260
+ * performance history graphs
261
+
262
+ ---
263
+
264
+ ## License
265
+
266
+ MIT License
267
+
268
+ ---
269
+
270
+ ## Author
271
+
272
+ Created by DarkGlitch.
273
+
274
+ Rewind aims to become a personal timeline for Linux systems.
@@ -0,0 +1,19 @@
1
+ database.py,sha256=jEEnNslQ9AJx-Ag29jql8wgyrDg52aBJbkBgr-yTQGA,3116
2
+ rewind.py,sha256=dfy_iV4jEcFsKOak8meqad_i_2PDRzwYWxcFtvFdqR0,1475
3
+ collectors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ collectors/collect.py,sha256=uxVsd8yP-S1faGVoLfaKtYMR_Iy_9hfM07k-LSaSOwU,1349
5
+ collectors/files.py,sha256=0uFEgq2FeRuz184D-IolHk5OXMviSMmKkMwioyOwAEA,2748
6
+ collectors/packages.py,sha256=yM_I1h10CcagxqLat8f6FR-nFrpBCqSR8Axk8sZS0Jo,1686
7
+ collectors/performance.py,sha256=csT3aOoCX28fYH0UNalIU2xjnQ6lHn8vZDf_5bcteUk,1452
8
+ collectors/services.py,sha256=PkmXJxDDk7jlcq1fTBXEZe8O7IpYE_wnLN5yOzI3P6g,1298
9
+ collectors/shell.py,sha256=23ykBbNMTup4i2PBxJ-j8wmjbiqkmeDJuEdK2RXoe-A,1487
10
+ commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ commands/search.py,sha256=7kJZXJZnXrd28fx9hqP_-PzdcXO5Zwl1B7CB_MQ36ns,515
12
+ commands/stats.py,sha256=UpQvCqzaDSTZY9boz7aaX5alNJeA-U6wfi6K3NLDQVY,988
13
+ commands/today.py,sha256=LJ0B2kB-iinAlFjKGs5yrncszDFA7VxqUGAU_2sX1e0,595
14
+ commands/yesterday.py,sha256=3T91mOAQhTff6RHIJGoV3LghvvCMh5WKypd5ugUksj8,678
15
+ rewind_timeline-0.1.0.dist-info/licenses/LICENSE,sha256=zRp28a4eWydIgwuAKXlfZPfHFcpLZh5eHVjAhiC6crU,1067
16
+ rewind_timeline-0.1.0.dist-info/METADATA,sha256=XazGnysTja1Wal3Q-VhJ_rjsBaS9mDh9X4ZVknemjng,3733
17
+ rewind_timeline-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
18
+ rewind_timeline-0.1.0.dist-info/top_level.txt,sha256=jyAjhXtmhL_E25CUy3kPoeWOnSxauknzUKwkFqhjAbY,36
19
+ rewind_timeline-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DarkGlitch
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.
@@ -0,0 +1,4 @@
1
+ collectors
2
+ commands
3
+ database
4
+ rewind