@allansantos-dev/smart-tool 0.9.45 → 0.9.46
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 +19 -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 +923 -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 +106 -49
- package/update_check.py +28 -10
- package/update_splash.py +197 -0
- package/version.py +1 -1
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.46",
|
|
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, **values):
|
|
86
|
+
"""Windows notification, translated (text is an English catalog key). A toast under Smart Tool's own app id,
|
|
87
|
+
since the tray balloon is shown as "Python"; when Windows refuses the toast, the balloon carries the text and the
|
|
88
|
+
refusal."""
|
|
89
|
+
message = i18n.t(text, **values)
|
|
90
|
+
try:
|
|
91
|
+
toast_notice.show("Smart Tool", [message], [])
|
|
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,19 @@ 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
|
-
icon
|
|
190
|
+
_notify(icon, "Smart Tool updated from {old} to {new}.", old=updated["from"], new=updated["to"])
|
|
163
191
|
failed = update_check.failed_update()
|
|
164
192
|
if failed:
|
|
165
|
-
icon
|
|
166
|
-
|
|
193
|
+
_notify(icon, "The update to {target} failed; Smart Tool {version} is still installed. It is tried again in "
|
|
194
|
+
"24 h, or now from this menu.", target=failed, version=version.VERSION)
|
|
167
195
|
|
|
168
196
|
|
|
169
197
|
def _update_loop(icon):
|
|
@@ -172,7 +200,7 @@ def _update_loop(icon):
|
|
|
172
200
|
try:
|
|
173
201
|
_announce_start(icon)
|
|
174
202
|
except Exception as exc:
|
|
175
|
-
icon
|
|
203
|
+
_notify(icon, "Could not read the update state: {error}", error=exc)
|
|
176
204
|
while True:
|
|
177
205
|
try:
|
|
178
206
|
latest = update_check.newer_version()
|
|
@@ -182,7 +210,7 @@ def _update_loop(icon):
|
|
|
182
210
|
try:
|
|
183
211
|
update_check.record_check(latest)
|
|
184
212
|
except Exception as exc:
|
|
185
|
-
icon
|
|
213
|
+
_notify(icon, "Could not save the update check: {error}", error=exc)
|
|
186
214
|
if latest:
|
|
187
215
|
known = latest == _state["update"]
|
|
188
216
|
_state["update"] = latest
|
|
@@ -191,38 +219,44 @@ def _update_loop(icon):
|
|
|
191
219
|
automatic = update_check.automatic_enabled() and update_check.should_auto_update(latest)
|
|
192
220
|
except Exception as exc:
|
|
193
221
|
automatic = False
|
|
194
|
-
icon
|
|
222
|
+
_notify(icon, "Automatic update skipped: {error}", error=exc)
|
|
195
223
|
if automatic:
|
|
196
|
-
icon
|
|
224
|
+
_notify(icon, "Updating Smart Tool from {old} to {new}...", old=version.VERSION, new=latest)
|
|
197
225
|
update_check.run_update(latest, automatic=True)
|
|
198
226
|
elif not known:
|
|
199
|
-
icon
|
|
227
|
+
_notify(icon, "Smart Tool {version} is available: use \"Update to {version}\" in this menu.",
|
|
228
|
+
version=latest)
|
|
200
229
|
time.sleep(UPDATE_CHECK_INTERVAL_S)
|
|
201
230
|
|
|
202
231
|
|
|
203
232
|
def _update_label(item):
|
|
204
|
-
return
|
|
233
|
+
return i18n.t("Update to {version}", version=_state["update"]) if _state["update"] else i18n.t("Check for updates")
|
|
205
234
|
|
|
206
235
|
|
|
207
236
|
_update_busy = threading.Lock()
|
|
208
237
|
|
|
209
238
|
|
|
210
239
|
def _update_now(icon, item):
|
|
211
|
-
"""Menu action: installs the known newer version, or checks npm first
|
|
240
|
+
"""Menu action: installs the known newer version, or checks npm first (telling the user it is checking) and says
|
|
241
|
+
when there is none."""
|
|
212
242
|
if not _update_busy.acquire(blocking=False):
|
|
213
243
|
return
|
|
214
244
|
|
|
215
245
|
def run():
|
|
216
246
|
try:
|
|
217
|
-
latest = _state["update"]
|
|
247
|
+
latest = _state["update"]
|
|
218
248
|
if not latest:
|
|
219
|
-
icon
|
|
249
|
+
_notify(icon, "Checking for updates…")
|
|
250
|
+
latest = update_check.newer_version()
|
|
251
|
+
if not latest:
|
|
252
|
+
_notify(icon, "Smart Tool {version} is the latest version.", version=version.VERSION)
|
|
220
253
|
return
|
|
221
254
|
_state["update"] = latest
|
|
222
255
|
icon.update_menu()
|
|
256
|
+
_notify(icon, "Updating Smart Tool from {old} to {new}...", old=version.VERSION, new=latest)
|
|
223
257
|
update_check.run_update(latest, automatic=False)
|
|
224
258
|
except Exception as exc:
|
|
225
|
-
icon
|
|
259
|
+
_notify(icon, "Could not update: {error}", error=exc)
|
|
226
260
|
finally:
|
|
227
261
|
_update_busy.release()
|
|
228
262
|
threading.Thread(target=run, daemon=True).start()
|
|
@@ -237,16 +271,34 @@ def _auto_update_checked(item):
|
|
|
237
271
|
|
|
238
272
|
def _toggle_auto_update(icon, item):
|
|
239
273
|
if update_check.env_opted_out():
|
|
240
|
-
icon
|
|
241
|
-
"Smart Tool")
|
|
274
|
+
_notify(icon, "{name}=off is set: automatic updates stay off until it is removed.", name=update_check.ENV_OPT_OUT)
|
|
242
275
|
return
|
|
243
276
|
try:
|
|
244
277
|
config.update_config(lambda cfg: cfg.__setitem__("auto_update", not config.auto_update(cfg)))
|
|
245
278
|
except Exception as exc:
|
|
246
|
-
icon
|
|
279
|
+
_notify(icon, "Could not change automatic updates: {error}", error=exc)
|
|
247
280
|
icon.update_menu()
|
|
248
281
|
|
|
249
282
|
|
|
283
|
+
def _language_checked(lang):
|
|
284
|
+
def checked(item):
|
|
285
|
+
try:
|
|
286
|
+
return i18n.language() == lang
|
|
287
|
+
except Exception:
|
|
288
|
+
return False
|
|
289
|
+
return checked
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _choose_language(lang):
|
|
293
|
+
def choose(icon, item):
|
|
294
|
+
try:
|
|
295
|
+
i18n.set_language(lang)
|
|
296
|
+
except Exception as exc:
|
|
297
|
+
_notify(icon, "Could not change the language: {error}", error=exc)
|
|
298
|
+
_follow_language(icon)
|
|
299
|
+
return choose
|
|
300
|
+
|
|
301
|
+
|
|
250
302
|
def _open_setup(icon, item):
|
|
251
303
|
registry = daemon_launcher.read_registry()
|
|
252
304
|
webbrowser.open((registry["url"].rstrip("/") + "/setup") if registry else SETUP_URL)
|
|
@@ -258,7 +310,7 @@ def _open_projects(icon, item):
|
|
|
258
310
|
url = daemon_launcher.ensure_daemon_running()
|
|
259
311
|
webbrowser.open(url.rstrip("/") + "/projects")
|
|
260
312
|
except Exception as exc:
|
|
261
|
-
icon
|
|
313
|
+
_notify(icon, "Could not open the projects: {error}", error=i18n.t(str(exc)))
|
|
262
314
|
threading.Thread(target=run, daemon=True).start()
|
|
263
315
|
|
|
264
316
|
|
|
@@ -268,7 +320,7 @@ def _add_project(icon, item):
|
|
|
268
320
|
import folder_picker
|
|
269
321
|
folder_picker.register_from_tray()
|
|
270
322
|
except Exception as exc:
|
|
271
|
-
icon
|
|
323
|
+
_notify(icon, "Could not select the folder: {error}", error=exc)
|
|
272
324
|
threading.Thread(target=run, daemon=True).start()
|
|
273
325
|
|
|
274
326
|
|
|
@@ -293,12 +345,10 @@ def _toggle_autostart(icon, item):
|
|
|
293
345
|
else:
|
|
294
346
|
result = autostart.install()
|
|
295
347
|
if result.get("method") == "startup_folder" and result.get("fallback_reason"):
|
|
296
|
-
icon.
|
|
297
|
-
|
|
298
|
-
"Smart Tool",
|
|
299
|
-
)
|
|
348
|
+
_notify(icon, "Scheduled Task unavailable ({reason}); using the Startup folder.",
|
|
349
|
+
reason=result["fallback_reason"])
|
|
300
350
|
except Exception as exc:
|
|
301
|
-
icon
|
|
351
|
+
_notify(icon, "Failed to change automatic start: {error}", error=exc)
|
|
302
352
|
finally:
|
|
303
353
|
_autostart_busy.release()
|
|
304
354
|
icon.update_menu()
|
|
@@ -332,18 +382,25 @@ def main():
|
|
|
332
382
|
handle = _single_instance_handle()
|
|
333
383
|
if handle is False:
|
|
334
384
|
return
|
|
385
|
+
_state["language"] = i18n.language()
|
|
335
386
|
icon = pystray.Icon(
|
|
336
387
|
"smart-tool",
|
|
337
388
|
icon=_ICONS["starting"],
|
|
338
|
-
title=_TITLES["starting"],
|
|
389
|
+
title=i18n.t(_TITLES["starting"]),
|
|
339
390
|
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),
|
|
391
|
+
pystray.MenuItem(lambda item: i18n.t("Projects and indexing…"), _open_projects, default=True),
|
|
392
|
+
pystray.MenuItem(lambda item: i18n.t("Index a folder…"), _add_project),
|
|
393
|
+
pystray.MenuItem(lambda item: i18n.t("Open settings"), _open_setup),
|
|
394
|
+
pystray.MenuItem(lambda item: i18n.t("Start with Windows"), _toggle_autostart, checked=_autostart_checked),
|
|
344
395
|
pystray.MenuItem(_update_label, _update_now),
|
|
345
|
-
pystray.MenuItem("Update automatically", _toggle_auto_update,
|
|
346
|
-
|
|
396
|
+
pystray.MenuItem(lambda item: i18n.t("Update automatically"), _toggle_auto_update,
|
|
397
|
+
checked=_auto_update_checked),
|
|
398
|
+
pystray.MenuItem("Idioma / Language", pystray.Menu(
|
|
399
|
+
pystray.MenuItem("Português (Brasil)", _choose_language("pt-BR"), checked=_language_checked("pt-BR"),
|
|
400
|
+
radio=True),
|
|
401
|
+
pystray.MenuItem("English", _choose_language("en"), checked=_language_checked("en"), radio=True),
|
|
402
|
+
)),
|
|
403
|
+
pystray.MenuItem(lambda item: i18n.t("Quit"), _quit),
|
|
347
404
|
),
|
|
348
405
|
)
|
|
349
406
|
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
|
|
|
@@ -102,6 +103,22 @@ def note_running_version(now=None):
|
|
|
102
103
|
return found
|
|
103
104
|
|
|
104
105
|
|
|
106
|
+
def update_for_tray(now=None):
|
|
107
|
+
"""The update record (from, to, at) of the running version the first time the tray asks, whether the tray or the
|
|
108
|
+
daemon started first and recorded it; None when there is none, it is older than the updated notice, or the tray
|
|
109
|
+
already told it."""
|
|
110
|
+
note_running_version(now)
|
|
111
|
+
now = now or time.time()
|
|
112
|
+
state = _state()
|
|
113
|
+
updated = state.get("updated") or {}
|
|
114
|
+
if (updated.get("to") != version.VERSION or state.get("tray_told") == version.VERSION
|
|
115
|
+
or now - updated.get("at", 0) >= UPDATED_NOTICE_S):
|
|
116
|
+
return None
|
|
117
|
+
state["tray_told"] = version.VERSION
|
|
118
|
+
_save(state)
|
|
119
|
+
return updated
|
|
120
|
+
|
|
121
|
+
|
|
105
122
|
def failed_update():
|
|
106
123
|
"""The version an automatic update went for and did not install (the previous version is still running), else
|
|
107
124
|
None."""
|
|
@@ -136,15 +153,16 @@ def notices(now=None):
|
|
|
136
153
|
|
|
137
154
|
|
|
138
155
|
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."""
|
|
156
|
+
"""Starts the update screen (update_splash.py) detached from the tray, which the installer stops and restarts:
|
|
157
|
+
it runs the install command with no console window and shows each step and the result. npx is looked up by the
|
|
158
|
+
screen when it starts, not in the PATH the tray was born with."""
|
|
143
159
|
if automatic:
|
|
144
160
|
state = _state()
|
|
145
161
|
state.update(attempted=latest, at=time.time())
|
|
146
162
|
_save(state)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
subprocess
|
|
150
|
-
|
|
163
|
+
folder = os.path.dirname(os.path.abspath(__file__))
|
|
164
|
+
pythonw = os.path.join(os.path.dirname(sys.executable), "pythonw.exe")
|
|
165
|
+
flags = getattr(subprocess, "DETACHED_PROCESS", 0) | getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
|
166
|
+
subprocess.Popen([pythonw if os.path.isfile(pythonw) else sys.executable, os.path.join(folder, "update_splash.py"),
|
|
167
|
+
latest], creationflags=flags, close_fds=True, cwd=folder, stdin=subprocess.DEVNULL,
|
|
168
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|