@allansantos-dev/smart-tool 0.9.45 → 0.9.47
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.
- package/CHANGELOG.md +27 -0
- package/README.md +9 -4
- package/autostart.py +5 -3
- package/client_hooks.py +1 -1
- package/config.py +12 -0
- package/folder_picker.py +7 -5
- package/i18n.py +213 -0
- package/locales/pt-BR.json +926 -0
- package/notice_action.py +8 -0
- package/package.json +1 -1
- package/projects_ui.py +7 -2
- package/setup_audit.py +5 -3
- package/setup_ui.py +15 -2
- package/smart_tool_daemon.py +1 -0
- package/toast_notice.py +3 -2
- package/tray.py +108 -49
- package/update_check.py +29 -10
- package/update_splash.py +197 -0
- package/version.py +1 -1
package/notice_action.py
CHANGED
|
@@ -33,6 +33,14 @@ def run(uri):
|
|
|
33
33
|
import daemon_launcher
|
|
34
34
|
webbrowser.open(daemon_launcher.ensure_daemon_running().rstrip("/") + "/setup#" + params.get("section", "models"))
|
|
35
35
|
return "setup opened"
|
|
36
|
+
if action == "open-update-log":
|
|
37
|
+
import update_check
|
|
38
|
+
os.startfile(update_check.UPDATE_LOG)
|
|
39
|
+
return "update log opened"
|
|
40
|
+
if action == "open-changes":
|
|
41
|
+
import update_check
|
|
42
|
+
webbrowser.open(update_check.SITE_URL)
|
|
43
|
+
return "changes opened"
|
|
36
44
|
if action == "mute-model":
|
|
37
45
|
import model_health
|
|
38
46
|
return model_health.mute(params.get("model", ""))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allansantos-dev/smart-tool",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.47",
|
|
4
4
|
"description": "Local MCP server that gives coding agents (Claude Code, Codex) cheaper, sharper tools than their built-in search. Windows.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Allan Santos",
|
package/projects_ui.py
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"""Tela local de projetos; todas as operações passam pelos jobs do daemon."""
|
|
2
2
|
import html
|
|
3
3
|
import re
|
|
4
|
+
import i18n
|
|
4
5
|
import setup_ui
|
|
5
6
|
import project_maps_ui
|
|
6
7
|
import project_insights_ui
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
def render_projects_page():
|
|
11
|
+
"""The projects page with its token, map and insight components and the interface language script."""
|
|
10
12
|
return (_PAGE.replace("__TOKEN__", html.escape(setup_ui.SETUP_TOKEN, quote=True))
|
|
13
|
+
.replace("__I18N__", i18n.page_script(setup_ui.SETUP_TOKEN))
|
|
11
14
|
.replace("__MAP_STYLE__", project_maps_ui.STYLE)
|
|
12
15
|
.replace("__MAP_HTML__", project_maps_ui.HTML)
|
|
13
16
|
.replace("__MAP_SCRIPT__", project_maps_ui.SCRIPT)
|
|
@@ -24,13 +27,14 @@ def render_map_window():
|
|
|
24
27
|
'__STYLE__': style.replace('__MAP_STYLE__', project_maps_ui.STYLE).replace('__INSIGHTS_STYLE__', project_insights_ui.STYLE),
|
|
25
28
|
'__MAP_HTML__': project_maps_ui.HTML,
|
|
26
29
|
'__MAP_SCRIPT__': project_maps_ui.SCRIPT,
|
|
30
|
+
'__I18N__': i18n.page_script(setup_ui.SETUP_TOKEN),
|
|
27
31
|
}
|
|
28
32
|
return re.sub('|'.join(re.escape(key) for key in values), lambda m:values[m[0]], _MAP_WINDOW)
|
|
29
33
|
|
|
30
34
|
|
|
31
35
|
_MAP_WINDOW = r'''<!doctype html>
|
|
32
36
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
33
|
-
<meta name="setup-token" content="__TOKEN__"><title>Project maps · Smart Tool</title
|
|
37
|
+
<meta name="setup-token" content="__TOKEN__"><title>Project maps · Smart Tool</title>__I18N__<style>
|
|
34
38
|
__STYLE__
|
|
35
39
|
body.map-window{padding:0}.map-window header,.map-window main{max-width:none}.map-window header{padding:22px 28px 14px}.map-window main{padding:0 20px 30px}.map-window h1{font-size:22px}.map-window #window-root{margin:6px 0 0;font-size:12px;overflow-wrap:anywhere}.map-window .maps{margin-top:0}.map-window .maps>summary{display:none}.map-window .maps-workspace{grid-template-columns:minmax(0,1fr) 285px}.map-window #maps-svg{height:calc(100vh - 360px);min-height:430px}.map-window .maps-tools #maps-new-window{display:none}.map-window #maps-color-legend{max-height:90px}.map-window #window-message:empty{display:none}.map-window #window-message{margin-bottom:14px;overflow-wrap:anywhere}
|
|
36
40
|
@media(max-width:850px){.map-window .maps-workspace{grid-template-columns:1fr}.map-window header{padding-inline:18px}.map-window main{padding-inline:12px}.map-window #maps-svg{height:450px;min-height:0}}
|
|
@@ -51,6 +55,7 @@ _PAGE = r'''<!doctype html>
|
|
|
51
55
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
52
56
|
<meta name="setup-token" content="__TOKEN__">
|
|
53
57
|
<title>Projects and indexing · Smart Tool</title>
|
|
58
|
+
__I18N__
|
|
54
59
|
<style>
|
|
55
60
|
:root{color-scheme:light dark;--bg:#f6f7f9;--surface:#fff;--border:#dde1e6;--ink:#1b2430;--muted:#5b6472;--accent:#2f6fed;--good:#18723c;--bad:#b52c2c;--warn:#805300;--selected:#eaf1ff}
|
|
56
61
|
@media(prefers-color-scheme:dark){:root{--bg:#14171c;--surface:#1c2027;--border:#39414e;--ink:#e6e9ee;--muted:#a6afbb;--accent:#91baff;--good:#68dc93;--bad:#ff9696;--warn:#e0b565;--selected:#263751}}
|
|
@@ -96,7 +101,7 @@ function setAdd(open){const restore=$('add-form').contains(document.activeElemen
|
|
|
96
101
|
function select(id){selected=id;scopeKey='';scopeDirty=false;window.SmartMaps?.reset();window.SmartInsights?.reset();history.replaceState(null,'','#'+id);render()}
|
|
97
102
|
function jobsHtml(rows){return rows.map(j=>`<div class="job"><div class="job-line"><strong>${esc(kindNames[j.kind]||j.kind)}</strong><span class="state ${j.status==='error'?'bad':''}">${esc(states[j.status]||j.status)}</span></div>${j.message?`<p>${esc(j.cancel_requested?'Cancellation requested; waiting for the current call to finish.':j.message)}</p>`:''}${j.status==='pending'?`<progress aria-label="Indexing progress" ${j.total_files>0&&j.processed_files!=null?`max="${j.total_files}" value="${j.processed_files}"`:''}></progress>`:''}${j.error?`<p class="state bad">${esc(j.error)}</p>`:''}${j.warning?`<p>${esc(j.warning)}</p>`:''}<small>${esc(j.job_id)}${j.elapsed_s!=null?' · '+j.elapsed_s+' s':''}</small></div>`).join('')}
|
|
98
103
|
function render(){
|
|
99
|
-
$('count').textContent=projects.length
|
|
104
|
+
$('count').textContent=projects.length===1?'1 folder registered':projects.length+' folders registered';
|
|
100
105
|
const list=$('project-list');
|
|
101
106
|
for(const node of [...list.children])if(!projects.some(p=>p.id===node.dataset.project))node.remove();
|
|
102
107
|
for(const p of projects){let button=list.querySelector(`[data-project="${p.id}"]`);if(!button){button=document.createElement('button');button.className='project-item';button.dataset.project=p.id;button.append(document.createElement('strong'),document.createElement('small'));list.append(button)}button.setAttribute('aria-current',String(p.id===selected));button.firstChild.textContent=p.name;button.lastChild.textContent=(states[p.status]||p.status)+(p.watch&&!p.paused?' · Watching':'')}
|
package/setup_audit.py
CHANGED
|
@@ -31,6 +31,7 @@ import yaml
|
|
|
31
31
|
|
|
32
32
|
import atomic_io
|
|
33
33
|
import endpoint_sync
|
|
34
|
+
import i18n
|
|
34
35
|
import paths
|
|
35
36
|
|
|
36
37
|
REPORT_DIR = os.path.join(paths.DATA_DIR, "setup-audit")
|
|
@@ -904,13 +905,14 @@ def notices_due():
|
|
|
904
905
|
|
|
905
906
|
|
|
906
907
|
def toast_text(row):
|
|
907
|
-
"""(title, lines) of the toast for one `notices_due` row."""
|
|
908
|
+
"""(title, lines) of the toast for one `notices_due` row, in the interface language."""
|
|
908
909
|
total = row["errors"] + row["warnings"]
|
|
909
|
-
title =
|
|
910
|
+
title = (i18n.t("Setup audit: 1 new problem in the agent configuration") if total == 1
|
|
911
|
+
else i18n.t("Setup audit: {count} new problems in the agent configuration", count=total))
|
|
910
912
|
lines = [os.path.basename(row["project"].rstrip("\\/")) or row["project"]]
|
|
911
913
|
lines += [f"{item['severity']}: {item['check']} - {item['subject']}"[:120] for item in row["items"][:3]]
|
|
912
914
|
if len(row["items"]) > 3:
|
|
913
|
-
lines.append(
|
|
915
|
+
lines.append(i18n.t("and {count} more", count=len(row["items"]) - 3))
|
|
914
916
|
return title, lines
|
|
915
917
|
|
|
916
918
|
|
package/setup_ui.py
CHANGED
|
@@ -28,6 +28,7 @@ import config
|
|
|
28
28
|
import daemon_launcher
|
|
29
29
|
import endpoint_sync
|
|
30
30
|
import gateway
|
|
31
|
+
import i18n
|
|
31
32
|
import model_catalog
|
|
32
33
|
import model_defaults
|
|
33
34
|
import model_health
|
|
@@ -101,6 +102,7 @@ def render_setup_page():
|
|
|
101
102
|
# Único valor que entra no corpo do HTML, e não no <script>: escapado porque vem
|
|
102
103
|
# de variável de ambiente, ainda que só quem já executa código possa definí-la.
|
|
103
104
|
"__SETUP_ORIGIN__": html.escape(setup_origin(), quote=True),
|
|
105
|
+
"__I18N__": i18n.page_script(SETUP_TOKEN),
|
|
104
106
|
}
|
|
105
107
|
# Um único passe sobre o template original: .replace() encadeado re-escanearia o
|
|
106
108
|
# próprio resultado e corromperia o JSON se um valor coincidisse com o outro placeholder.
|
|
@@ -419,6 +421,16 @@ def handle_providers_save(body):
|
|
|
419
421
|
"validated_results": validated}
|
|
420
422
|
|
|
421
423
|
|
|
424
|
+
def handle_language_save(body):
|
|
425
|
+
"""Saves the interface language the language button picked; returns {"language": <saved>}."""
|
|
426
|
+
if not isinstance(body, dict):
|
|
427
|
+
raise SetupError("Request body must be a JSON object with 'language'.")
|
|
428
|
+
try:
|
|
429
|
+
return {"language": i18n.set_language(body.get("language"))}
|
|
430
|
+
except ValueError as exc:
|
|
431
|
+
raise SetupError(str(exc)) from exc
|
|
432
|
+
|
|
433
|
+
|
|
422
434
|
def handle_autostart_get():
|
|
423
435
|
return autostart.status()
|
|
424
436
|
|
|
@@ -494,6 +506,7 @@ _PAGE_TEMPLATE = """<!doctype html>
|
|
|
494
506
|
<meta charset="utf-8">
|
|
495
507
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
496
508
|
<title>Settings · Smart Tool</title>
|
|
509
|
+
__I18N__
|
|
497
510
|
<style>
|
|
498
511
|
:root { color-scheme: light dark;
|
|
499
512
|
--bg: #f6f7f9; --surface: #ffffff; --line: #dde1e6; --ink: #1b2430; --muted: #5b6472; --soft: #eef1f5;
|
|
@@ -665,7 +678,7 @@ _PAGE_TEMPLATE = """<!doctype html>
|
|
|
665
678
|
</head>
|
|
666
679
|
<body>
|
|
667
680
|
<header class="top">
|
|
668
|
-
<div><h1>Smart Tool settings</h1><span class="where">__SETUP_ORIGIN__ ·
|
|
681
|
+
<div><h1>Smart Tool settings</h1><span class="where">__SETUP_ORIGIN__ · <span>Version</span> <span id="app-version"></span></span></div>
|
|
669
682
|
<nav><a href="/projects">Projects and indexing →</a></nav>
|
|
670
683
|
</header>
|
|
671
684
|
<div class="shell">
|
|
@@ -1010,7 +1023,7 @@ function renderOverview() {
|
|
|
1010
1023
|
badge.hidden = !entry;
|
|
1011
1024
|
badge.textContent = entry ? entry.count : "";
|
|
1012
1025
|
badge.dataset.tone = entry && entry.bad ? "bad" : "warn";
|
|
1013
|
-
badge.title = entry ?
|
|
1026
|
+
badge.title = !entry ? "" : entry.count === 1 ? "1 item needs attention" : entry.count + " items need attention";
|
|
1014
1027
|
}
|
|
1015
1028
|
}
|
|
1016
1029
|
|
package/smart_tool_daemon.py
CHANGED
|
@@ -4163,6 +4163,7 @@ _SETUP_SAVES = {
|
|
|
4163
4163
|
"/setup/gateway": setup_ui.handle_gateway_save,
|
|
4164
4164
|
"/setup/autostart": setup_ui.handle_autostart_action,
|
|
4165
4165
|
"/setup/contact": setup_ui.handle_contact_save,
|
|
4166
|
+
"/setup/language": setup_ui.handle_language_save,
|
|
4166
4167
|
"/setup/audit": setup_ui.handle_audit,
|
|
4167
4168
|
}
|
|
4168
4169
|
|
package/toast_notice.py
CHANGED
|
@@ -74,13 +74,14 @@ def action_uri(action, **params):
|
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
def toast_xml(title, lines, buttons):
|
|
77
|
-
"""`buttons` is a list of (label, uri); each one launches its protocol link and closes the toast.
|
|
77
|
+
"""`buttons` is a list of (label, uri); each one launches its protocol link and closes the toast. An empty list
|
|
78
|
+
gives a toast without an actions element."""
|
|
78
79
|
text = "".join(f"<text>{escape(line)}</text>" for line in [title, *lines])
|
|
79
80
|
actions = "".join(
|
|
80
81
|
f'<action content={quoteattr(label)} arguments={quoteattr(uri)} activationType="protocol"/>'
|
|
81
82
|
for label, uri in buttons)
|
|
82
83
|
return (f'<toast><visual><binding template="ToastGeneric">{text}</binding></visual>'
|
|
83
|
-
f'<actions>{actions}</actions
|
|
84
|
+
+ (f'<actions>{actions}</actions>' if actions else '') + '</toast>')
|
|
84
85
|
|
|
85
86
|
|
|
86
87
|
def show(title, lines, buttons, tag=None):
|
package/tray.py
CHANGED
|
@@ -22,6 +22,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
22
22
|
import autostart
|
|
23
23
|
import config
|
|
24
24
|
import daemon_launcher
|
|
25
|
+
import i18n
|
|
25
26
|
import setup_audit
|
|
26
27
|
import toast_notice
|
|
27
28
|
import update_check
|
|
@@ -60,14 +61,14 @@ _ICONS = {
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
_TITLES = {
|
|
63
|
-
"ok": "Smart Tool -
|
|
64
|
-
"starting": "Smart Tool - starting
|
|
65
|
-
"needs_key": "Smart Tool - model gateway unavailable",
|
|
66
|
-
"needs_models": "Smart Tool - choose the models in settings",
|
|
67
|
-
"catalog_unavailable": "Smart Tool - model catalog unavailable",
|
|
68
|
-
"config_corrupt": "Smart Tool - settings file has an error",
|
|
69
|
-
"status_error": "Smart Tool - failed to read the settings",
|
|
70
|
-
"down": "Smart Tool -
|
|
64
|
+
"ok": i18n.N_("Smart Tool - running"),
|
|
65
|
+
"starting": i18n.N_("Smart Tool - starting..."),
|
|
66
|
+
"needs_key": i18n.N_("Smart Tool - model gateway unavailable"),
|
|
67
|
+
"needs_models": i18n.N_("Smart Tool - choose the models in settings"),
|
|
68
|
+
"catalog_unavailable": i18n.N_("Smart Tool - model catalog unavailable"),
|
|
69
|
+
"config_corrupt": i18n.N_("Smart Tool - settings file has an error"),
|
|
70
|
+
"status_error": i18n.N_("Smart Tool - failed to read the settings"),
|
|
71
|
+
"down": i18n.N_("Smart Tool - stopped"),
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
|
|
@@ -78,7 +79,30 @@ def _set_state(health, icon=None):
|
|
|
78
79
|
_state["health"] = health
|
|
79
80
|
if icon is not None:
|
|
80
81
|
icon.icon = _ICONS[health]
|
|
81
|
-
icon.title = _TITLES[health]
|
|
82
|
+
icon.title = i18n.t(_TITLES[health])
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _notify(icon, text, buttons=(), **values):
|
|
86
|
+
"""Windows notification, translated (text is an English catalog key; buttons are (catalog key, protocol link)). A
|
|
87
|
+
toast under Smart Tool's own app id, since the tray balloon is shown as "Python"; when Windows refuses the toast,
|
|
88
|
+
the balloon carries the text and the refusal."""
|
|
89
|
+
message = i18n.t(text, **values)
|
|
90
|
+
try:
|
|
91
|
+
toast_notice.show("Smart Tool", [message], [(i18n.t(label), uri) for label, uri in buttons])
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
icon.notify(f"{message} ({exc})", "Smart Tool")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _follow_language(icon):
|
|
97
|
+
"""Redraws the menu and the tooltip after the language changed here or in a settings page."""
|
|
98
|
+
try:
|
|
99
|
+
current = i18n.language()
|
|
100
|
+
except Exception:
|
|
101
|
+
return
|
|
102
|
+
if _state.get("language") != current:
|
|
103
|
+
_state["language"] = current
|
|
104
|
+
icon.title = i18n.t(_TITLES[_state["health"]])
|
|
105
|
+
icon.update_menu()
|
|
82
106
|
|
|
83
107
|
|
|
84
108
|
def _configuration_state(url):
|
|
@@ -111,6 +135,7 @@ def _poll_loop(icon):
|
|
|
111
135
|
_set_state("status_error", icon)
|
|
112
136
|
except Exception:
|
|
113
137
|
_set_state("down", icon)
|
|
138
|
+
_follow_language(icon)
|
|
114
139
|
time.sleep(POLL_INTERVAL_S)
|
|
115
140
|
|
|
116
141
|
|
|
@@ -121,19 +146,22 @@ def _show_notices(icon, notices):
|
|
|
121
146
|
if offer:
|
|
122
147
|
lines = [f"{role}: {change['current']} -> {change['new']}" for role, change in offer["changes"].items()]
|
|
123
148
|
nonce = offer["nonce"]
|
|
124
|
-
toast_notice.show("Smart Tool has new recommended models", lines, [
|
|
125
|
-
("Apply now", toast_notice.action_uri("models-apply", n=nonce)),
|
|
126
|
-
("Don't show again", toast_notice.action_uri("models-decline", n=nonce)),
|
|
149
|
+
toast_notice.show(i18n.t("Smart Tool has new recommended models"), lines, [
|
|
150
|
+
(i18n.t("Apply now"), toast_notice.action_uri("models-apply", n=nonce)),
|
|
151
|
+
(i18n.t("Don't show again"), toast_notice.action_uri("models-decline", n=nonce)),
|
|
127
152
|
], tag="models-upgrade")
|
|
128
153
|
for row in notices.get("failing") or []:
|
|
129
|
-
last = row.get("last_error") or "see the failure log in the settings"
|
|
154
|
+
last = row.get("last_error") or i18n.t("see the failure log in the settings")
|
|
130
155
|
toast_notice.show(
|
|
131
|
-
|
|
132
|
-
[("
|
|
133
|
-
|
|
156
|
+
i18n.t("{model} is failing", model=row["model"]),
|
|
157
|
+
[i18n.t("{errors} of {calls} calls failed in 24 h as {role}.", errors=row["errors"], calls=row["calls"],
|
|
158
|
+
role=row["role"]), last[:160]],
|
|
159
|
+
[(i18n.t("Switch model"), toast_notice.action_uri("open-setup", section="models")),
|
|
160
|
+
(i18n.t("Mute 7 days"), toast_notice.action_uri("mute-model", model=row["model"]))],
|
|
161
|
+
tag="model-failing:" + row["model"])
|
|
134
162
|
for row in notices.get("audit") or []:
|
|
135
163
|
title, lines = setup_audit.toast_text(row)
|
|
136
|
-
toast_notice.show(title, lines, [("Open the audit", toast_notice.action_uri("open-setup", section="audit"))],
|
|
164
|
+
toast_notice.show(title, lines, [(i18n.t("Open the audit"), toast_notice.action_uri("open-setup", section="audit"))],
|
|
137
165
|
tag="setup-audit:" + row["project"])
|
|
138
166
|
|
|
139
167
|
|
|
@@ -151,19 +179,21 @@ def _notice_loop(icon):
|
|
|
151
179
|
except Exception as exc:
|
|
152
180
|
if str(exc) != _state.get("notice_error"):
|
|
153
181
|
_state["notice_error"] = str(exc)
|
|
154
|
-
icon
|
|
182
|
+
_notify(icon, "Could not show a Smart Tool notice: {error}", error=exc)
|
|
155
183
|
time.sleep(NOTICE_INTERVAL_S)
|
|
156
184
|
|
|
157
185
|
|
|
158
186
|
def _announce_start(icon):
|
|
159
187
|
"""Tells the user, once per start, that Smart Tool was just updated or that the last automatic update failed."""
|
|
160
|
-
updated = update_check.
|
|
188
|
+
updated = update_check.update_for_tray()
|
|
161
189
|
if updated:
|
|
162
|
-
|
|
190
|
+
changes = (i18n.N_("What changed"), toast_notice.action_uri("open-changes"))
|
|
191
|
+
_notify(icon, "Smart Tool updated from {old} to {new}.", [changes], old=updated["from"], new=updated["to"])
|
|
163
192
|
failed = update_check.failed_update()
|
|
164
193
|
if failed:
|
|
165
|
-
|
|
166
|
-
|
|
194
|
+
log = (i18n.N_("View log"), toast_notice.action_uri("open-update-log"))
|
|
195
|
+
_notify(icon, "The update to {target} failed; Smart Tool {version} is still installed. It is tried again in "
|
|
196
|
+
"24 h, or now from this menu.", [log], target=failed, version=version.VERSION)
|
|
167
197
|
|
|
168
198
|
|
|
169
199
|
def _update_loop(icon):
|
|
@@ -172,7 +202,7 @@ def _update_loop(icon):
|
|
|
172
202
|
try:
|
|
173
203
|
_announce_start(icon)
|
|
174
204
|
except Exception as exc:
|
|
175
|
-
icon
|
|
205
|
+
_notify(icon, "Could not read the update state: {error}", error=exc)
|
|
176
206
|
while True:
|
|
177
207
|
try:
|
|
178
208
|
latest = update_check.newer_version()
|
|
@@ -182,7 +212,7 @@ def _update_loop(icon):
|
|
|
182
212
|
try:
|
|
183
213
|
update_check.record_check(latest)
|
|
184
214
|
except Exception as exc:
|
|
185
|
-
icon
|
|
215
|
+
_notify(icon, "Could not save the update check: {error}", error=exc)
|
|
186
216
|
if latest:
|
|
187
217
|
known = latest == _state["update"]
|
|
188
218
|
_state["update"] = latest
|
|
@@ -191,38 +221,44 @@ def _update_loop(icon):
|
|
|
191
221
|
automatic = update_check.automatic_enabled() and update_check.should_auto_update(latest)
|
|
192
222
|
except Exception as exc:
|
|
193
223
|
automatic = False
|
|
194
|
-
icon
|
|
224
|
+
_notify(icon, "Automatic update skipped: {error}", error=exc)
|
|
195
225
|
if automatic:
|
|
196
|
-
icon
|
|
226
|
+
_notify(icon, "Updating Smart Tool from {old} to {new}...", old=version.VERSION, new=latest)
|
|
197
227
|
update_check.run_update(latest, automatic=True)
|
|
198
228
|
elif not known:
|
|
199
|
-
icon
|
|
229
|
+
_notify(icon, "Smart Tool {version} is available: use \"Update to {version}\" in this menu.",
|
|
230
|
+
version=latest)
|
|
200
231
|
time.sleep(UPDATE_CHECK_INTERVAL_S)
|
|
201
232
|
|
|
202
233
|
|
|
203
234
|
def _update_label(item):
|
|
204
|
-
return
|
|
235
|
+
return i18n.t("Update to {version}", version=_state["update"]) if _state["update"] else i18n.t("Check for updates")
|
|
205
236
|
|
|
206
237
|
|
|
207
238
|
_update_busy = threading.Lock()
|
|
208
239
|
|
|
209
240
|
|
|
210
241
|
def _update_now(icon, item):
|
|
211
|
-
"""Menu action: installs the known newer version, or checks npm first
|
|
242
|
+
"""Menu action: installs the known newer version, or checks npm first (telling the user it is checking) and says
|
|
243
|
+
when there is none."""
|
|
212
244
|
if not _update_busy.acquire(blocking=False):
|
|
213
245
|
return
|
|
214
246
|
|
|
215
247
|
def run():
|
|
216
248
|
try:
|
|
217
|
-
latest = _state["update"]
|
|
249
|
+
latest = _state["update"]
|
|
218
250
|
if not latest:
|
|
219
|
-
icon
|
|
251
|
+
_notify(icon, "Checking for updates…")
|
|
252
|
+
latest = update_check.newer_version()
|
|
253
|
+
if not latest:
|
|
254
|
+
_notify(icon, "Smart Tool {version} is the latest version.", version=version.VERSION)
|
|
220
255
|
return
|
|
221
256
|
_state["update"] = latest
|
|
222
257
|
icon.update_menu()
|
|
258
|
+
_notify(icon, "Updating Smart Tool from {old} to {new}...", old=version.VERSION, new=latest)
|
|
223
259
|
update_check.run_update(latest, automatic=False)
|
|
224
260
|
except Exception as exc:
|
|
225
|
-
icon
|
|
261
|
+
_notify(icon, "Could not update: {error}", error=exc)
|
|
226
262
|
finally:
|
|
227
263
|
_update_busy.release()
|
|
228
264
|
threading.Thread(target=run, daemon=True).start()
|
|
@@ -237,16 +273,34 @@ def _auto_update_checked(item):
|
|
|
237
273
|
|
|
238
274
|
def _toggle_auto_update(icon, item):
|
|
239
275
|
if update_check.env_opted_out():
|
|
240
|
-
icon
|
|
241
|
-
"Smart Tool")
|
|
276
|
+
_notify(icon, "{name}=off is set: automatic updates stay off until it is removed.", name=update_check.ENV_OPT_OUT)
|
|
242
277
|
return
|
|
243
278
|
try:
|
|
244
279
|
config.update_config(lambda cfg: cfg.__setitem__("auto_update", not config.auto_update(cfg)))
|
|
245
280
|
except Exception as exc:
|
|
246
|
-
icon
|
|
281
|
+
_notify(icon, "Could not change automatic updates: {error}", error=exc)
|
|
247
282
|
icon.update_menu()
|
|
248
283
|
|
|
249
284
|
|
|
285
|
+
def _language_checked(lang):
|
|
286
|
+
def checked(item):
|
|
287
|
+
try:
|
|
288
|
+
return i18n.language() == lang
|
|
289
|
+
except Exception:
|
|
290
|
+
return False
|
|
291
|
+
return checked
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _choose_language(lang):
|
|
295
|
+
def choose(icon, item):
|
|
296
|
+
try:
|
|
297
|
+
i18n.set_language(lang)
|
|
298
|
+
except Exception as exc:
|
|
299
|
+
_notify(icon, "Could not change the language: {error}", error=exc)
|
|
300
|
+
_follow_language(icon)
|
|
301
|
+
return choose
|
|
302
|
+
|
|
303
|
+
|
|
250
304
|
def _open_setup(icon, item):
|
|
251
305
|
registry = daemon_launcher.read_registry()
|
|
252
306
|
webbrowser.open((registry["url"].rstrip("/") + "/setup") if registry else SETUP_URL)
|
|
@@ -258,7 +312,7 @@ def _open_projects(icon, item):
|
|
|
258
312
|
url = daemon_launcher.ensure_daemon_running()
|
|
259
313
|
webbrowser.open(url.rstrip("/") + "/projects")
|
|
260
314
|
except Exception as exc:
|
|
261
|
-
icon
|
|
315
|
+
_notify(icon, "Could not open the projects: {error}", error=i18n.t(str(exc)))
|
|
262
316
|
threading.Thread(target=run, daemon=True).start()
|
|
263
317
|
|
|
264
318
|
|
|
@@ -268,7 +322,7 @@ def _add_project(icon, item):
|
|
|
268
322
|
import folder_picker
|
|
269
323
|
folder_picker.register_from_tray()
|
|
270
324
|
except Exception as exc:
|
|
271
|
-
icon
|
|
325
|
+
_notify(icon, "Could not select the folder: {error}", error=exc)
|
|
272
326
|
threading.Thread(target=run, daemon=True).start()
|
|
273
327
|
|
|
274
328
|
|
|
@@ -293,12 +347,10 @@ def _toggle_autostart(icon, item):
|
|
|
293
347
|
else:
|
|
294
348
|
result = autostart.install()
|
|
295
349
|
if result.get("method") == "startup_folder" and result.get("fallback_reason"):
|
|
296
|
-
icon.
|
|
297
|
-
|
|
298
|
-
"Smart Tool",
|
|
299
|
-
)
|
|
350
|
+
_notify(icon, "Scheduled Task unavailable ({reason}); using the Startup folder.",
|
|
351
|
+
reason=result["fallback_reason"])
|
|
300
352
|
except Exception as exc:
|
|
301
|
-
icon
|
|
353
|
+
_notify(icon, "Failed to change automatic start: {error}", error=exc)
|
|
302
354
|
finally:
|
|
303
355
|
_autostart_busy.release()
|
|
304
356
|
icon.update_menu()
|
|
@@ -332,18 +384,25 @@ def main():
|
|
|
332
384
|
handle = _single_instance_handle()
|
|
333
385
|
if handle is False:
|
|
334
386
|
return
|
|
387
|
+
_state["language"] = i18n.language()
|
|
335
388
|
icon = pystray.Icon(
|
|
336
389
|
"smart-tool",
|
|
337
390
|
icon=_ICONS["starting"],
|
|
338
|
-
title=_TITLES["starting"],
|
|
391
|
+
title=i18n.t(_TITLES["starting"]),
|
|
339
392
|
menu=pystray.Menu(
|
|
340
|
-
pystray.MenuItem("Projects and indexing…", _open_projects, default=True),
|
|
341
|
-
pystray.MenuItem("Index a folder…", _add_project),
|
|
342
|
-
pystray.MenuItem("Open settings", _open_setup),
|
|
343
|
-
pystray.MenuItem("Start with Windows", _toggle_autostart, checked=_autostart_checked),
|
|
393
|
+
pystray.MenuItem(lambda item: i18n.t("Projects and indexing…"), _open_projects, default=True),
|
|
394
|
+
pystray.MenuItem(lambda item: i18n.t("Index a folder…"), _add_project),
|
|
395
|
+
pystray.MenuItem(lambda item: i18n.t("Open settings"), _open_setup),
|
|
396
|
+
pystray.MenuItem(lambda item: i18n.t("Start with Windows"), _toggle_autostart, checked=_autostart_checked),
|
|
344
397
|
pystray.MenuItem(_update_label, _update_now),
|
|
345
|
-
pystray.MenuItem("Update automatically", _toggle_auto_update,
|
|
346
|
-
|
|
398
|
+
pystray.MenuItem(lambda item: i18n.t("Update automatically"), _toggle_auto_update,
|
|
399
|
+
checked=_auto_update_checked),
|
|
400
|
+
pystray.MenuItem("Idioma / Language", pystray.Menu(
|
|
401
|
+
pystray.MenuItem("Português (Brasil)", _choose_language("pt-BR"), checked=_language_checked("pt-BR"),
|
|
402
|
+
radio=True),
|
|
403
|
+
pystray.MenuItem("English", _choose_language("en"), checked=_language_checked("en"), radio=True),
|
|
404
|
+
)),
|
|
405
|
+
pystray.MenuItem(lambda item: i18n.t("Quit"), _quit),
|
|
347
406
|
),
|
|
348
407
|
)
|
|
349
408
|
threading.Thread(target=_poll_loop, args=(icon,), daemon=True).start()
|
package/update_check.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Newer Smart Tool release on npm and the update itself: the tray checks npm at start and every 24 h and, when
|
|
2
|
-
automatic updates are on (the default), runs the same install command a user would,
|
|
2
|
+
automatic updates are on (the default), runs the same install command a user would, behind the update screen
|
|
3
|
+
(update_splash.py) and with no console window. A failed
|
|
3
4
|
version is tried again after 24 h. What happened (newer version available, updated, update failed) is told to the
|
|
4
5
|
user by the tray and to agent sessions by the daemon, from the state file, without contacting npm again.
|
|
5
6
|
|
|
@@ -8,12 +9,12 @@ newer-version notice still shows then, with the command to update by hand."""
|
|
|
8
9
|
import json
|
|
9
10
|
import os
|
|
10
11
|
import subprocess
|
|
12
|
+
import sys
|
|
11
13
|
import time
|
|
12
14
|
import urllib.request
|
|
13
15
|
|
|
14
16
|
import atomic_io
|
|
15
17
|
import config
|
|
16
|
-
import node_locator
|
|
17
18
|
import paths
|
|
18
19
|
import version
|
|
19
20
|
|
|
@@ -22,6 +23,7 @@ REGISTRY_URL = "https://registry.npmjs.org/@allansantos-dev%2Fsmart-tool/latest"
|
|
|
22
23
|
UPDATE_COMMAND = f"npx --yes {PACKAGE}@latest install"
|
|
23
24
|
SITE_URL = "https://allansantos-dv.github.io/smart-tool/"
|
|
24
25
|
STATE_PATH = os.path.join(paths.DATA_DIR, "update-state.json")
|
|
26
|
+
UPDATE_LOG = os.path.join(paths.DATA_DIR, "update.log")
|
|
25
27
|
ENV_OPT_OUT = "SMART_TOOL_AUTO_UPDATE"
|
|
26
28
|
RETRY_AFTER_S = 24 * 3600
|
|
27
29
|
UPDATED_NOTICE_S = 24 * 3600
|
|
@@ -102,6 +104,22 @@ def note_running_version(now=None):
|
|
|
102
104
|
return found
|
|
103
105
|
|
|
104
106
|
|
|
107
|
+
def update_for_tray(now=None):
|
|
108
|
+
"""The update record (from, to, at) of the running version the first time the tray asks, whether the tray or the
|
|
109
|
+
daemon started first and recorded it; None when there is none, it is older than the updated notice, or the tray
|
|
110
|
+
already told it."""
|
|
111
|
+
note_running_version(now)
|
|
112
|
+
now = now or time.time()
|
|
113
|
+
state = _state()
|
|
114
|
+
updated = state.get("updated") or {}
|
|
115
|
+
if (updated.get("to") != version.VERSION or state.get("tray_told") == version.VERSION
|
|
116
|
+
or now - updated.get("at", 0) >= UPDATED_NOTICE_S):
|
|
117
|
+
return None
|
|
118
|
+
state["tray_told"] = version.VERSION
|
|
119
|
+
_save(state)
|
|
120
|
+
return updated
|
|
121
|
+
|
|
122
|
+
|
|
105
123
|
def failed_update():
|
|
106
124
|
"""The version an automatic update went for and did not install (the previous version is still running), else
|
|
107
125
|
None."""
|
|
@@ -136,15 +154,16 @@ def notices(now=None):
|
|
|
136
154
|
|
|
137
155
|
|
|
138
156
|
def run_update(latest, automatic):
|
|
139
|
-
"""Starts the
|
|
140
|
-
the
|
|
141
|
-
|
|
142
|
-
found is put first in the PATH the installer (and the tray it restarts) inherits."""
|
|
157
|
+
"""Starts the update screen (update_splash.py) detached from the tray, which the installer stops and restarts:
|
|
158
|
+
it runs the install command with no console window and shows each step and the result. npx is looked up by the
|
|
159
|
+
screen when it starts, not in the PATH the tray was born with."""
|
|
143
160
|
if automatic:
|
|
144
161
|
state = _state()
|
|
145
162
|
state.update(attempted=latest, at=time.time())
|
|
146
163
|
_save(state)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
subprocess
|
|
150
|
-
|
|
164
|
+
folder = os.path.dirname(os.path.abspath(__file__))
|
|
165
|
+
pythonw = os.path.join(os.path.dirname(sys.executable), "pythonw.exe")
|
|
166
|
+
flags = getattr(subprocess, "DETACHED_PROCESS", 0) | getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
|
167
|
+
subprocess.Popen([pythonw if os.path.isfile(pythonw) else sys.executable, os.path.join(folder, "update_splash.py"),
|
|
168
|
+
latest], creationflags=flags, close_fds=True, cwd=folder, stdin=subprocess.DEVNULL,
|
|
169
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|