feat: sync mixly static resources, tools and sw-mixly
BIN
mixly/files/add.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
mixly/files/background.jpg
Normal file
|
After Width: | Height: | Size: 153 KiB |
BIN
mixly/files/blank.png
Normal file
|
After Width: | Height: | Size: 732 B |
5
mixly/files/bootstrap.min.css
vendored
Normal file
BIN
mixly/files/default.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
mixly/files/fonts/pxiByp8kv8JHgFVrLCz7Z1xlFQ.woff2
Normal file
BIN
mixly/files/fonts/pxiByp8kv8JHgFVrLGT9Z1xlFQ.woff2
Normal file
BIN
mixly/files/fonts/pxiEyp8kv8JHgFVrJJfecg.woff2
Normal file
BIN
mixly/files/icons/mixly-192.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
mixly/files/icons/mixly-512.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
1
mixly/files/jquery.mb.YTPlayer.min.css
vendored
Normal file
1
mixly/files/magnificpopup.css
Normal file
BIN
mixly/files/mixly.icns
Normal file
BIN
mixly/files/mixly.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
mixly/files/mixly_uncompressed.ico
Normal file
|
After Width: | Height: | Size: 66 KiB |
6
mixly/files/owl.carousel.min.css
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Owl Carousel v2.2.1
|
||||||
|
* Copyright 2013-2017 David Deutsch
|
||||||
|
* Licensed under ()
|
||||||
|
*/
|
||||||
|
.owl-carousel,.owl-carousel .owl-item{-webkit-tap-highlight-color:transparent;position:relative}.owl-carousel{display:none;width:100%;z-index:1}.owl-carousel .owl-stage{position:relative;-ms-touch-action:pan-Y;-moz-backface-visibility:hidden}.owl-carousel .owl-stage:after{content:".";display:block;clear:both;visibility:hidden;line-height:0;height:0}.owl-carousel .owl-stage-outer{position:relative;overflow:hidden;-webkit-transform:translate3d(0,0,0)}.owl-carousel .owl-item,.owl-carousel .owl-wrapper{-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0)}.owl-carousel .owl-item{min-height:1px;float:left;-webkit-backface-visibility:hidden;-webkit-touch-callout:none}.owl-carousel .owl-item img{display:block;width:100%}.owl-carousel .owl-dots.disabled,.owl-carousel .owl-nav.disabled{display:none}.no-js .owl-carousel,.owl-carousel.owl-loaded{display:block}.owl-carousel .owl-dot,.owl-carousel .owl-nav .owl-next,.owl-carousel .owl-nav .owl-prev{cursor:pointer;cursor:hand;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.owl-carousel.owl-loading{opacity:0;display:block}.owl-carousel.owl-hidden{opacity:0}.owl-carousel.owl-refresh .owl-item{visibility:hidden}.owl-carousel.owl-drag .owl-item{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.owl-carousel.owl-grab{cursor:move;cursor:grab}.owl-carousel.owl-rtl{direction:rtl}.owl-carousel.owl-rtl .owl-item{float:right}.owl-carousel .animated{animation-duration:1s;animation-fill-mode:both}.owl-carousel .owl-animated-in{z-index:0}.owl-carousel .owl-animated-out{z-index:1}.owl-carousel .fadeOut{animation-name:fadeOut}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}.owl-height{transition:height .5s ease-in-out}.owl-carousel .owl-item .owl-lazy{opacity:0;transition:opacity .4s ease}.owl-carousel .owl-item img.owl-lazy{transform-style:preserve-3d}.owl-carousel .owl-video-wrapper{position:relative;height:100%;background:#000}.owl-carousel .owl-video-play-icon{position:absolute;height:80px;width:80px;left:50%;top:50%;margin-left:-40px;margin-top:-40px;background:url(owl.video.play.png) no-repeat;cursor:pointer;z-index:1;-webkit-backface-visibility:hidden;transition:transform .1s ease}.owl-carousel .owl-video-play-icon:hover{-ms-transform:scale(1.3,1.3);transform:scale(1.3,1.3)}.owl-carousel .owl-video-playing .owl-video-play-icon,.owl-carousel .owl-video-playing .owl-video-tn{display:none}.owl-carousel .owl-video-tn{opacity:0;height:100%;background-position:center center;background-repeat:no-repeat;background-size:contain;transition:opacity .4s ease}.owl-carousel .owl-video-frame{position:relative;z-index:1;height:100%;width:100%}
|
||||||
1
mixly/files/responsive.css
Normal file
5
mixly/files/slicknav.min.css
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/*!
|
||||||
|
* SlickNav Responsive Mobile Menu v1.0.10
|
||||||
|
* (c) 2016 Josh Cope
|
||||||
|
* licensed under MIT
|
||||||
|
*/.slicknav_btn,.slicknav_nav .slicknav_item{cursor:pointer}.slicknav_menu,.slicknav_menu *{box-sizing:border-box}.slicknav_btn{position:relative;display:block;vertical-align:middle;float:right;padding:.438em .625em;line-height:1.125em}.slicknav_btn .slicknav_icon-bar+.slicknav_icon-bar{margin-top:.188em}.slicknav_menu .slicknav_menutxt{display:block;line-height:1.188em;float:left;color:#fff;font-weight:700;text-shadow:0 1px 3px #000}.slicknav_menu .slicknav_icon{float:left;width:1.125em;height:.875em;margin:.188em 0 0 .438em}.slicknav_menu .slicknav_icon:before{background:0 0;width:1.125em;height:.875em;display:block;content:"";position:absolute}.slicknav_menu .slicknav_no-text{margin:0}.slicknav_menu .slicknav_icon-bar{display:block;width:1.125em;height:.125em;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,.25);box-shadow:0 1px 0 rgba(0,0,0,.25)}.slicknav_menu:after,.slicknav_menu:before{content:" ";display:table}.slicknav_menu:after{clear:both}.slicknav_nav li,.slicknav_nav ul{display:block}.slicknav_nav .slicknav_arrow{font-size:.8em;margin:0 0 0 .4em}.slicknav_nav .slicknav_item a{display:inline}.slicknav_nav .slicknav_row,.slicknav_nav a{display:block}.slicknav_nav .slicknav_parent-link a{display:inline}.slicknav_menu{*zoom:1;font-size:16px;background:#4c4c4c;padding:5px}.slicknav_nav,.slicknav_nav ul{list-style:none;overflow:hidden;padding:0}.slicknav_menu .slicknav_icon-bar{background-color:#fff}.slicknav_btn{margin:5px 5px 6px;text-decoration:none;text-shadow:0 1px 1px rgba(255,255,255,.75);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;background-color:#222}.slicknav_nav{clear:both;color:#fff;margin:0;font-size:.875em}.slicknav_nav ul{margin:0 0 0 20px}.slicknav_nav .slicknav_row,.slicknav_nav a{padding:5px 10px;margin:2px 5px}.slicknav_nav .slicknav_row:hover{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;background:#ccc;color:#fff}.slicknav_nav a{text-decoration:none;color:#fff}.slicknav_nav a:hover{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;background:#ccc;color:#222}.slicknav_nav .slicknav_txtnode{margin-left:15px}.slicknav_nav .slicknav_item a,.slicknav_nav .slicknav_parent-link a{padding:0;margin:0}.slicknav_brand{float:left;color:#fff;font-size:18px;line-height:30px;padding:7px 12px;height:44px}
|
||||||
1
mixly/files/style.css
Normal file
269
mixly/files/themes.css
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
:root {
|
||||||
|
--app-light-color: #009688;
|
||||||
|
--app-dark-color: #007acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.layui-form.layui-border-box.layui-table-view {
|
||||||
|
margin:0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.layui-form.layui-border-box.layui-table-view {
|
||||||
|
margin: 0px !important;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
border-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.layui-form.layui-border-box.layui-table-view > div.layui-table-tool {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 15px;
|
||||||
|
height: 50px;
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.layui-form.layui-border-box.layui-table-view > div.layui-table-box {
|
||||||
|
position: absolute;
|
||||||
|
overflow-y: hidden;
|
||||||
|
overflow-x: hidden;
|
||||||
|
bottom: 0px;
|
||||||
|
width: 100%;
|
||||||
|
left: 0px;
|
||||||
|
top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*div.layui-form.layui-border-box.layui-table-view > div.layui-table-box > div.layui-table-header {
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
height: 38px;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom-width: 0px;
|
||||||
|
border-right-width: 17px;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
div.layui-form.layui-border-box.layui-table-view > div.layui-table-tool > div.layui-table-tool-temp {
|
||||||
|
padding-right: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.layui-form.layui-border-box.layui-table-view > div.layui-table-box > div.layui-table-header > table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.layui-form.layui-border-box.layui-table-view > div.layui-table-box > div.layui-table-body.layui-table-main {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: scroll;
|
||||||
|
position: absolute;
|
||||||
|
top: 38px;
|
||||||
|
left: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*div.layui-form.layui-border-box.layui-table-view > div.layui-table-box > div > table > thead > tr > th > div > div {
|
||||||
|
top:6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.layui-form.layui-border-box.layui-table-view > div.layui-table-box > div > table > tbody > tr > td > div > div {
|
||||||
|
top:6px !important;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
div.layui-form.layui-border-box.layui-table-view > div.layui-table-box > div.layui-table-body.layui-table-main > table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-table-body {
|
||||||
|
margin-right: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-layer-resize {
|
||||||
|
width: 5px !important;
|
||||||
|
height: 10px !important;
|
||||||
|
border-radius: 0px 0px 5px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] .layui-layer-resize {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-table-body {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-table-header {
|
||||||
|
border-right-width: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] .layui-carousel[lay-indicator=outside] .layui-carousel-ind ul {
|
||||||
|
background-color: rgb(141 135 135 / 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] .layui-carousel[lay-indicator=outside] .layui-carousel-ind ul {
|
||||||
|
background-color: rgb(130 129 129 / 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] #footer > div > p {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] .layui-carousel-arrow {
|
||||||
|
background-color: rgba(0,0,0,.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] .layui-carousel-arrow:hover, html[data-bs-theme=light] .layui-carousel-ind ul:hover {
|
||||||
|
background-color: rgba(0,0,0,.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] .service-single {
|
||||||
|
background: #fff;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
box-shadow: 1px 1px 4px rgb(0 0 0 / 15%);
|
||||||
|
border-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] .service-single h2 {
|
||||||
|
color: #232323;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] #footer > div > p {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] .service-single {
|
||||||
|
background: #2d2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] .service-single h2 {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[class="light"] {
|
||||||
|
background-color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[class="dark"] {
|
||||||
|
background-color: #181818 !important;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mixly-board:hover .mixly-board-del-btn, .mixly-board:hover .mixly-board-ignore-btn {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*.mixly-board:hover .service-single label {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mixly-board .service-single label {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mixly-board:hover .service-single h2 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] .mixly-board .service-single label {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] .mixly-board .service-single label {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mixly-board-del-btn, .mixly-board-ignore-btn {
|
||||||
|
display: none;
|
||||||
|
background-color: rgba(0,0,0,0);
|
||||||
|
border: 0px;
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-right: 0px;
|
||||||
|
position: absolute;
|
||||||
|
right: 15px;
|
||||||
|
top: 5px;
|
||||||
|
opacity: 0.5;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] .mixly-board-del-btn, html[data-bs-theme=dark] .mixly-board-ignore-btn {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mixly-board-del-btn:hover, .mixly-board-ignore-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] .layui-progress-text {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-table-fixed-r > .layui-table-header {
|
||||||
|
border-bottom-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-table-fixed-r > .layui-table-body > table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] .layui-table-body .layui-none {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] .layui-layer-setwin span {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] .layui-layer-setwin span {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] .layui-carousel-arrow {
|
||||||
|
background-color: #2d2c2c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] .layui-carousel-arrow:hover,
|
||||||
|
html[data-bs-theme=dark] .layui-carousel-ind ul:hover {
|
||||||
|
background-color: rgba(119,119,119,.35) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100000;
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] .loading {
|
||||||
|
background-color: #f9f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] .loading {
|
||||||
|
background-color: #181818;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] .select2-container--open .select2-selection--single {
|
||||||
|
outline: 1px solid var(--app-light-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] .select2-container--open .select2-selection--single {
|
||||||
|
outline: 1px solid var(--app-dark-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] > body > .select2-container > .select2-dropdown {
|
||||||
|
outline: 1px solid var(--app-light-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] > body > .select2-container > .select2-dropdown {
|
||||||
|
outline: 1px solid var(--app-dark-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-single > a,
|
||||||
|
.service-single > a > img {
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
-moz-user-drag: none;
|
||||||
|
-ms-user-drag: none;
|
||||||
|
user-drag: none;
|
||||||
|
}
|
||||||
1
mixly/files/typography.css
Normal file
1292
mixly/mixly-sw/mixly-modules/common/board-manager.js
Normal file
66
mixly/mixly-sw/mixly-modules/common/config.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
(() => {
|
||||||
|
|
||||||
|
goog.require('Mixly.Url');
|
||||||
|
goog.require('Mixly.LocalStorage');
|
||||||
|
goog.provide('Mixly.Config');
|
||||||
|
|
||||||
|
const {
|
||||||
|
Url,
|
||||||
|
LocalStorage,
|
||||||
|
Config
|
||||||
|
} = Mixly;
|
||||||
|
|
||||||
|
Config.USER = {
|
||||||
|
theme: 'light',
|
||||||
|
language: 'zh-hans',
|
||||||
|
winSize: 1,
|
||||||
|
blockRenderer: 'geras',
|
||||||
|
compileCAndH: 'true',
|
||||||
|
boardIgnore: []
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function 读取软件、板卡的配置信息
|
||||||
|
* @return {void}
|
||||||
|
**/
|
||||||
|
Config.init = () => {
|
||||||
|
Config.SOFTWARE = goog.readJsonSync('./sw-config.json', {});
|
||||||
|
console.log('Config.SOFTWARE:', Config.SOFTWARE);
|
||||||
|
Config.BOARDS_INFO = goog.readJsonSync('./boards.json', {});
|
||||||
|
console.log('Config.BOARDS_INFO:', Config.BOARDS_INFO);
|
||||||
|
const boardPageConfig = Url.getConfig();
|
||||||
|
Config.BOARD_PAGE = boardPageConfig ?? {};
|
||||||
|
console.log('Config.BOARD_PAGE:', Config.BOARD_PAGE);
|
||||||
|
document.title = Config.SOFTWARE.version ?? 'Mixly 3.0';
|
||||||
|
|
||||||
|
Config.USER = {
|
||||||
|
...Config.USER,
|
||||||
|
...LocalStorage.get(LocalStorage.PATH['USER']) ?? {}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Config.USER.themeAuto) {
|
||||||
|
const themeMedia = window.matchMedia("(prefers-color-scheme: light)");
|
||||||
|
Config.USER.theme = themeMedia.matches ? 'light' : 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Config.USER.languageAuto) {
|
||||||
|
switch (navigator.language) {
|
||||||
|
case 'zh-CN':
|
||||||
|
Config.USER.language = 'zh-hans';
|
||||||
|
break;
|
||||||
|
case 'zh-HK':
|
||||||
|
case 'zh-SG':
|
||||||
|
case 'zh-TW':
|
||||||
|
Config.USER.language = 'zh-hant';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Config.USER.language = 'en';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Config.USER', Config.USER);
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.init();
|
||||||
|
|
||||||
|
})();
|
||||||
78
mixly/mixly-sw/mixly-modules/common/env.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
(() => {
|
||||||
|
|
||||||
|
goog.require('path');
|
||||||
|
goog.require('Mixly');
|
||||||
|
goog.require('Mixly.Config');
|
||||||
|
goog.provide('Mixly.Env');
|
||||||
|
|
||||||
|
const fs_extra = Mixly.require('fs-extra');
|
||||||
|
const fs_plus = Mixly.require('fs-plus');
|
||||||
|
const electron_remote = Mixly.require('@electron/remote');
|
||||||
|
|
||||||
|
const { Env, Config } = Mixly;
|
||||||
|
|
||||||
|
const { SOFTWARE } = Config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前mixly的路径
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
Env.clientPath = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测当前系统
|
||||||
|
* @type {String} win32、darwin、linux
|
||||||
|
*/
|
||||||
|
Env.currentPlatform = goog.platform();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取板卡index或主页面index的路径
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
Env.indexDirPath = path.join((new URL($('html')[0].baseURI)).href, '../').replace(/file:\/+/g, '');
|
||||||
|
Env.indexDirPath = decodeURIComponent(Env.indexDirPath);
|
||||||
|
if (Env.currentPlatform !== 'win32') {
|
||||||
|
Env.indexDirPath = '/' + Env.indexDirPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测是否启用node服务器
|
||||||
|
* @type {Boolean}
|
||||||
|
*/
|
||||||
|
Env.hasSocketServer = SOFTWARE?.webSocket?.enabled ? true : false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测是否启用node编译服务器
|
||||||
|
* @type {Boolean}
|
||||||
|
*/
|
||||||
|
Env.hasCompiler = SOFTWARE?.webCompiler?.enabled ? true : false;
|
||||||
|
|
||||||
|
Env.thirdPartyBoardPath = path.join(Env.indexDirPath, 'boards/extend');
|
||||||
|
|
||||||
|
if (goog.isElectron) {
|
||||||
|
const { app } = electron_remote;
|
||||||
|
const { currentPlatform } = Env;
|
||||||
|
if (currentPlatform === "darwin") {
|
||||||
|
Env.clientPath = path.join(app.getPath("exe"), '../../../../');
|
||||||
|
} else {
|
||||||
|
Env.clientPath = path.join(app.getPath("exe"), '../');
|
||||||
|
}
|
||||||
|
if (Env.currentPlatform === "darwin" || Env.currentPlatform === "linux") {
|
||||||
|
Env.python3Path = '/usr/bin/python3';
|
||||||
|
} else {
|
||||||
|
Env.python3Path = path.join(Env.clientPath, 'mixpyBuild/win_python3/python3.exe');
|
||||||
|
}
|
||||||
|
|
||||||
|
Env.arduinoCliPath = path.join(Env.clientPath, 'arduino-cli/');
|
||||||
|
const cliFilePath = path.join(Env.arduinoCliPath, 'arduino-cli' + (currentPlatform === 'win32'? '.exe':''));
|
||||||
|
if (!fs_plus.isFileSync(cliFilePath)) {
|
||||||
|
const defaultPath = SOFTWARE?.defaultPath[currentPlatform] ?? null;
|
||||||
|
if (defaultPath?.arduinoCli) {
|
||||||
|
Env.arduinoCliPath = path.join(Env.clientPath, defaultPath.arduinoCli, '../');
|
||||||
|
} else {
|
||||||
|
Env.arduinoCliPath = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})()
|
||||||
141
mixly/mixly-sw/mixly-modules/common/events.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
goog.loadJs('electron', () => {
|
||||||
|
|
||||||
|
goog.require('Mixly.BoardManager');
|
||||||
|
goog.require('Mixly.Env');
|
||||||
|
goog.require('Mixly.Config');
|
||||||
|
goog.require('Mixly.Url');
|
||||||
|
goog.provide('Mixly.Events');
|
||||||
|
|
||||||
|
const {
|
||||||
|
BoardManager,
|
||||||
|
Env,
|
||||||
|
Config,
|
||||||
|
Url,
|
||||||
|
Events
|
||||||
|
} = Mixly;
|
||||||
|
|
||||||
|
const fs = Mixly.require('fs');
|
||||||
|
const electron = Mixly.require('electron');
|
||||||
|
const electron_remote = Mixly.require('@electron/remote');
|
||||||
|
const { ipcRenderer } = electron;
|
||||||
|
const { USER } = Config;
|
||||||
|
|
||||||
|
ipcRenderer.on('ping', (event, message) => {
|
||||||
|
console.log(message);
|
||||||
|
var messageObj = null;
|
||||||
|
try {
|
||||||
|
messageObj = JSON.parse(message);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (messageObj?.type == "update") {
|
||||||
|
if (USER.autoUpdate !== 'no') {
|
||||||
|
const contentData = `<div style="padding: 50px; line-height: 22px; background-color: #393D49; color: #fff; font-weight: 300;text-align: center;">有可用更新,是否立即下载<br /><b style="font-size: 10px;color: #fff;">版本:${messageObj?.oldVersion} → ${messageObj?.newVersion}</b><br /><b style="color: #f70a2b;">注意:</b><br /><p style="color: #f70a2b;">更新时会关闭所有Mixly窗口!</p></div>`;
|
||||||
|
layer.open({
|
||||||
|
type: 1,
|
||||||
|
title: false,
|
||||||
|
closeBtn: false,
|
||||||
|
area: '300px',
|
||||||
|
shade: 0.8,
|
||||||
|
id: 'LAY_layuipro',
|
||||||
|
btn: ['稍后提醒', '立即更新'],
|
||||||
|
btnAlign: 'c',
|
||||||
|
moveType: 1,
|
||||||
|
content: contentData,
|
||||||
|
resize: false,
|
||||||
|
success: function (layero) {
|
||||||
|
},
|
||||||
|
btn2: function () {
|
||||||
|
ipcRenderer.send('ping', "update");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on('open-file', (event, message) => {
|
||||||
|
function getBoardFromXml(xml) {
|
||||||
|
if (xml.indexOf("board=\"") === -1) {
|
||||||
|
var idxa = xml.indexOf("board=\\\"") + 7;
|
||||||
|
var idxb = xml.indexOf("\"", idxa + 1);
|
||||||
|
if (idxa !== -1 && idxb !== -1 && idxb > idxa)
|
||||||
|
return xml.substring(idxa + 1, idxb - 1);
|
||||||
|
} else {
|
||||||
|
var idxa = xml.indexOf("board=\"") + 6;
|
||||||
|
var idxb = xml.indexOf("\"", idxa + 1);
|
||||||
|
if (idxa !== -1 && idxb !== -1 && idxb > idxa)
|
||||||
|
return xml.substring(idxa + 1, idxb);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let mixStr = fs.readFileSync(message, "utf8");
|
||||||
|
let boardType = getBoardFromXml(mixStr);
|
||||||
|
if (boardType && boardType.indexOf('@') !== -1) {
|
||||||
|
boardType = boardType.substring(0, boardType.indexOf('@'));
|
||||||
|
} else if (boardType && boardType.indexOf('/') !== -1) {
|
||||||
|
boardType = boardType.substring(0, boardType.indexOf('/'));
|
||||||
|
}
|
||||||
|
if (boardType) {
|
||||||
|
BoardManager.loadBoards();
|
||||||
|
const { boardsList } = BoardManager;
|
||||||
|
for (let i = 0; i < boardsList.length; i++) {
|
||||||
|
if (boardsList[i].boardType === boardType) {
|
||||||
|
boardsList[i].filePath = message;
|
||||||
|
const {
|
||||||
|
boardType,
|
||||||
|
boardIndex,
|
||||||
|
boardImg,
|
||||||
|
thirdPartyBoard,
|
||||||
|
filePath
|
||||||
|
} = boardsList[i];
|
||||||
|
let boardJson = JSON.parse(JSON.stringify({
|
||||||
|
boardType,
|
||||||
|
boardIndex,
|
||||||
|
boardImg,
|
||||||
|
thirdPartyBoard,
|
||||||
|
filePath
|
||||||
|
}));
|
||||||
|
let params = "id=error";
|
||||||
|
try {
|
||||||
|
params = Url.jsonToUrl(boardJson);
|
||||||
|
window.location.href = "./boards/index.html?" + params;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(function () {
|
||||||
|
alert("未找到" + boardType + "板卡!");
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
setTimeout(function () {
|
||||||
|
alert("未在文件内找到板卡名!");
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on('command', (event, command) => {
|
||||||
|
let commandObj = null;
|
||||||
|
try {
|
||||||
|
commandObj = JSON.parse(command);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const defaultCommand = {
|
||||||
|
obj: '',
|
||||||
|
func: '',
|
||||||
|
args: []
|
||||||
|
};
|
||||||
|
commandObj = {
|
||||||
|
...defaultCommand,
|
||||||
|
...commandObj
|
||||||
|
}
|
||||||
|
if (commandObj.obj === 'Mixly.Electron.Loader' && commandObj.func === 'reload') {
|
||||||
|
const currentWindow = electron_remote.getCurrentWindow();
|
||||||
|
currentWindow.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
99
mixly/mixly-sw/mixly-modules/common/loader.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
(() => {
|
||||||
|
|
||||||
|
goog.require('layui');
|
||||||
|
goog.require('Mixly.Url');
|
||||||
|
goog.require('Mixly.Env');
|
||||||
|
goog.require('Mixly.Config');
|
||||||
|
goog.require('Mixly.BoardManager');
|
||||||
|
goog.require('Mixly.XML');
|
||||||
|
goog.require('Mixly.Msg');
|
||||||
|
goog.require('Mixly.XML');
|
||||||
|
goog.require('Mixly.Setting');
|
||||||
|
goog.require('Mixly.Events');
|
||||||
|
goog.require('Mixly.Electron.PythonShell');
|
||||||
|
goog.require('Mixly.WebSocket.Socket');
|
||||||
|
goog.provide('Mixly.Loader');
|
||||||
|
|
||||||
|
const {
|
||||||
|
Url,
|
||||||
|
Env,
|
||||||
|
Config,
|
||||||
|
BoardManager,
|
||||||
|
XML,
|
||||||
|
Setting,
|
||||||
|
Electron,
|
||||||
|
Loader
|
||||||
|
} = Mixly;
|
||||||
|
|
||||||
|
const { carousel } = layui;
|
||||||
|
|
||||||
|
const { BOARD_PAGE } = Config;
|
||||||
|
|
||||||
|
const { PythonShell } = Electron;
|
||||||
|
|
||||||
|
Loader.init = () => {
|
||||||
|
$('body').append(XML.TEMPLATE_STR['INTERFACE']);
|
||||||
|
$('body').on('contextmenu', (e) => e.preventDefault());
|
||||||
|
if (goog.isElectron) {
|
||||||
|
PythonShell.init();
|
||||||
|
}
|
||||||
|
if (Env.hasSocketServer) {
|
||||||
|
const { Socket } = Mixly.WebSocket;
|
||||||
|
Socket.init();
|
||||||
|
}
|
||||||
|
BoardManager.loadBoards();
|
||||||
|
BoardManager.updateBoardsCard();
|
||||||
|
Setting.init();
|
||||||
|
window.addEventListener('resize', BoardManager.updateBoardsCard, false);
|
||||||
|
carousel.on('change(board-switch-filter)', function (obj) {
|
||||||
|
const boardType = obj.item.find('.mixly-board').find('h2').html() ?? 'Add';
|
||||||
|
history.replaceState({}, "", Url.changeURLArg(window.location.href, "boardType", boardType));
|
||||||
|
BOARD_PAGE.boardType = boardType;
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#loading").fadeOut("normal", () => {
|
||||||
|
$('#loading').remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (goog.isElectron || window.location.hostname.lastIndexOf('mixly.cn') === -1) {
|
||||||
|
(function(window, document) {
|
||||||
|
var url = 'https://mixly.org/public/app30.html';
|
||||||
|
if (!goog.isElectron) {
|
||||||
|
if (typeof nw === 'object') {
|
||||||
|
url = 'https://mixly.org/public/app32.html';
|
||||||
|
} else {
|
||||||
|
url = 'https://mixly.org/public/app31.html';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function detect() {
|
||||||
|
var iframes = document.getElementsByTagName('iframe');
|
||||||
|
for (var i = 0; i < iframes.length; i++) {
|
||||||
|
if (iframes[0].src === url) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function createIframe() {
|
||||||
|
if (detect()) return;
|
||||||
|
var i = document.createElement("iframe");
|
||||||
|
i.src = url;
|
||||||
|
i.width = '0';
|
||||||
|
i.height = '0';
|
||||||
|
i.style.display = 'none';
|
||||||
|
document.body.appendChild(i);
|
||||||
|
}
|
||||||
|
createIframe();
|
||||||
|
})(window, document);
|
||||||
|
} else {
|
||||||
|
(function() {
|
||||||
|
var hm = document.createElement("script");
|
||||||
|
hm.src = "https://hm.baidu.com/hm.js?3914f31c236391e8ad9780ff27a6ab23";
|
||||||
|
var s = document.getElementsByTagName("script")[0];
|
||||||
|
s.parentNode.insertBefore(hm, s);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
Loader.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
31
mixly/mixly-sw/mixly-modules/common/msg.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
(() => {
|
||||||
|
|
||||||
|
goog.require('Mixly.MJson');
|
||||||
|
goog.require('Mixly.Config');
|
||||||
|
goog.provide('Mixly.Msg');
|
||||||
|
|
||||||
|
const { Msg, MJson, Config } = Mixly;
|
||||||
|
|
||||||
|
const { USER } = Config;
|
||||||
|
|
||||||
|
Msg.LANG_PATH = {
|
||||||
|
"zh-hans": "./mixly-sw/msg/zh-hans.json",
|
||||||
|
"zh-hant": "./mixly-sw/msg/zh-hant.json",
|
||||||
|
"en": "./mixly-sw/msg/en.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
Msg.LANG = {
|
||||||
|
"zh-hans": MJson.get(Msg.LANG_PATH["zh-hans"]),
|
||||||
|
"zh-hant": MJson.get(Msg.LANG_PATH["zh-hant"]),
|
||||||
|
"en": MJson.get(Msg.LANG_PATH["en"])
|
||||||
|
}
|
||||||
|
|
||||||
|
Msg.nowLang = USER.language ?? 'zh-hans';
|
||||||
|
|
||||||
|
Msg.getLang = (str) => {
|
||||||
|
return Msg.LANG[Msg.nowLang][str];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Msg.LANG', Msg.LANG);
|
||||||
|
|
||||||
|
})();
|
||||||
345
mixly/mixly-sw/mixly-modules/common/setting.js
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
(() => {
|
||||||
|
|
||||||
|
goog.require('ace');
|
||||||
|
goog.require('ace.ExtLanguageTools');
|
||||||
|
goog.require('layui');
|
||||||
|
goog.require('store');
|
||||||
|
goog.require('$.select2');
|
||||||
|
goog.require('$.fomanticUI');
|
||||||
|
goog.require('Mixly.XML');
|
||||||
|
goog.require('Mixly.LayerExt');
|
||||||
|
goog.require('Mixly.Msg');
|
||||||
|
goog.require('Mixly.BoardManager');
|
||||||
|
goog.require('Mixly.Config');
|
||||||
|
goog.require('Mixly.Env');
|
||||||
|
goog.require('Mixly.MJson');
|
||||||
|
goog.require('Mixly.Storage');
|
||||||
|
goog.require('Mixly.WebSocket.Socket');
|
||||||
|
goog.provide('Mixly.Setting');
|
||||||
|
|
||||||
|
const {
|
||||||
|
XML,
|
||||||
|
LayerExt,
|
||||||
|
Msg,
|
||||||
|
BoardManager,
|
||||||
|
Config,
|
||||||
|
Env,
|
||||||
|
MJson,
|
||||||
|
Storage,
|
||||||
|
Setting
|
||||||
|
} = Mixly;
|
||||||
|
|
||||||
|
const { LANG } = Msg;
|
||||||
|
const { element, form, layer } = layui;
|
||||||
|
const { USER, SOFTWARE } = Config;
|
||||||
|
|
||||||
|
Setting.ID = 'setting-menu';
|
||||||
|
Setting.CONFIG = {}
|
||||||
|
Setting.nowIndex = 0;
|
||||||
|
Setting.config = {};
|
||||||
|
|
||||||
|
Setting.init = () => {
|
||||||
|
element.tab({
|
||||||
|
headerElem: '#setting-menu-options>li',
|
||||||
|
bodyElem: '#setting-menu-body>.menu-body'
|
||||||
|
});
|
||||||
|
element.render('nav', 'setting-menu-filter');
|
||||||
|
Setting.addOnchangeOptionListener();
|
||||||
|
|
||||||
|
form.on('switch(setting-theme-filter)', function(data) {
|
||||||
|
const { checked } = data.elem;
|
||||||
|
USER.theme = checked ? 'dark' : 'light';
|
||||||
|
$('body').removeClass('dark light')
|
||||||
|
.addClass(USER.theme);
|
||||||
|
$('html').attr('data-bs-theme', USER.theme);
|
||||||
|
Storage.user('/', USER);
|
||||||
|
});
|
||||||
|
|
||||||
|
form.on('submit(open-setting-dialog-filter)', function(data) {
|
||||||
|
Setting.onclick();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
form.on('submit(board-reset-filter)', function(data) {
|
||||||
|
BoardManager.resetBoard((error) => {
|
||||||
|
if (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
BoardManager.screenWidthLevel = -1;
|
||||||
|
BoardManager.screenHeightLevel = -1;
|
||||||
|
BoardManager.loadBoards();
|
||||||
|
BoardManager.updateBoardsCard();
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Setting.menuInit = () => {
|
||||||
|
$('#setting-menu-options').children('.layui-this').removeClass('layui-this');
|
||||||
|
$('#setting-menu-options').children('li').first().addClass('layui-this');
|
||||||
|
$('#setting-menu-body').children('.layui-show').removeClass('layui-show');
|
||||||
|
$('#setting-menu-body').children('div').first().addClass('layui-show');
|
||||||
|
form.render(null, 'setting-form-filter');
|
||||||
|
form.val('setting-form-filter', USER);
|
||||||
|
}
|
||||||
|
|
||||||
|
Setting.onclick = () => {
|
||||||
|
Setting.menuInit();
|
||||||
|
let obj = $(".setting-menu-item").select2({
|
||||||
|
width: '100%',
|
||||||
|
minimumResultsForSearch: 10
|
||||||
|
});
|
||||||
|
Setting.configMenuSetValue(obj, USER);
|
||||||
|
element.render('collapse', 'menu-user-collapse-filter');
|
||||||
|
Setting.nowIndex = 0;
|
||||||
|
LayerExt.open({
|
||||||
|
title: [Msg.getLang('SETTING'), '36px'],
|
||||||
|
id: 'setting-menu-layer',
|
||||||
|
content: $('#' + Setting.ID),
|
||||||
|
shade: LayerExt.SHADE_ALL,
|
||||||
|
area: ['50%', '50%'],
|
||||||
|
min: ['400px', '200px'],
|
||||||
|
success: () => {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('#setting-menu-user button').off().click((event) => {
|
||||||
|
const type = $(event.currentTarget).attr('value');
|
||||||
|
switch (type) {
|
||||||
|
case 'apply':
|
||||||
|
let oldTheme = USER.themeAuto? 'auto' : USER.theme;
|
||||||
|
let oldLanglage = USER.languageAuto? 'auto' : USER.language;
|
||||||
|
let updateTheme = false, updateLanguage = false;
|
||||||
|
let value = Setting.configMenuGetValue(obj);
|
||||||
|
for (let i in value) {
|
||||||
|
USER[i] = value[i];
|
||||||
|
}
|
||||||
|
updateTheme = oldTheme !== USER.theme;
|
||||||
|
updateLanguage = oldLanglage !== USER.language;
|
||||||
|
if (updateTheme) {
|
||||||
|
if (USER.theme === 'auto') {
|
||||||
|
const themeMedia = window.matchMedia("(prefers-color-scheme: light)");
|
||||||
|
USER.theme = themeMedia.matches ? 'light' : 'dark';
|
||||||
|
USER.themeAuto = true;
|
||||||
|
} else {
|
||||||
|
USER.themeAuto = false;
|
||||||
|
}
|
||||||
|
$('body').removeClass('dark light')
|
||||||
|
.addClass(USER.theme);
|
||||||
|
$('html').attr('data-bs-theme', USER.theme);
|
||||||
|
}
|
||||||
|
if (updateLanguage) {
|
||||||
|
if (USER.language === 'auto') {
|
||||||
|
switch (navigator.language) {
|
||||||
|
case 'zh-CN':
|
||||||
|
USER.language = 'zh-hans';
|
||||||
|
break;
|
||||||
|
case 'zh-HK':
|
||||||
|
case 'zh-SG':
|
||||||
|
case 'zh-TW':
|
||||||
|
USER.language = 'zh-hant';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
USER.language = 'en';
|
||||||
|
}
|
||||||
|
USER.languageAuto = true;
|
||||||
|
} else {
|
||||||
|
USER.languageAuto = false;
|
||||||
|
}
|
||||||
|
Msg.nowLang = USER.language ?? 'zh-hans';
|
||||||
|
}
|
||||||
|
if (updateTheme || updateLanguage) {
|
||||||
|
BoardManager.screenWidthLevel = -1;
|
||||||
|
BoardManager.screenHeightLevel = -1;
|
||||||
|
BoardManager.updateBoardsCard();
|
||||||
|
}
|
||||||
|
Storage.user('/', USER);
|
||||||
|
layer.closeAll(() => {
|
||||||
|
XML.renderAllTemplete();
|
||||||
|
layer.msg(Msg.getLang('CONFIG_UPDATE_SUCC'), { time: 1000 });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'reset':
|
||||||
|
Setting.configMenuReset(obj);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Setting.addOnchangeOptionListener = () => {
|
||||||
|
element.on('tab(setting-menu-filter)', function(data) {
|
||||||
|
const { index } = data;
|
||||||
|
const type = $(data.elem.prevObject).data('type');
|
||||||
|
if (type === 'import-board') {
|
||||||
|
if (data.index !== Setting.nowIndex) {
|
||||||
|
goog.isElectron && BoardManager.onclickImportBoards();
|
||||||
|
} else {
|
||||||
|
layui.table.resize('cloud-boards-table');
|
||||||
|
}
|
||||||
|
} else if (type === 'ws-update') {
|
||||||
|
if (data.index !== Setting.nowIndex) {
|
||||||
|
$('#setting-menu-update').loading({
|
||||||
|
background: USER.theme === 'dark' ? '#807b7b' : '#fff',
|
||||||
|
opacity: 1,
|
||||||
|
animateTime: 0,
|
||||||
|
imgSrc: 1
|
||||||
|
});
|
||||||
|
const { Socket } = Mixly.WebSocket;
|
||||||
|
Socket.updating = true;
|
||||||
|
Socket.sendCommand({
|
||||||
|
obj: 'Socket',
|
||||||
|
func: 'getConfigByUrl',
|
||||||
|
args: [ SOFTWARE.configUrl ]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (type === 'nw-update') {
|
||||||
|
fetch('/api/check-update', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((result) => {
|
||||||
|
const {
|
||||||
|
localVersion,
|
||||||
|
cloudVersion,
|
||||||
|
needsUpdate,
|
||||||
|
cloudFile
|
||||||
|
} = result;
|
||||||
|
Setting.refreshUpdateMenuStatus(localVersion, cloudVersion, needsUpdate, cloudFile);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Setting.nowIndex = index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Setting.configMenuReset = (obj) => {
|
||||||
|
for (let i = 0; i < obj.length; i++) {
|
||||||
|
let $item = $(obj[i]);
|
||||||
|
let newValue = $item.children('option').first().val();
|
||||||
|
$item.val(newValue).trigger("change");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Setting.configMenuSetValue = (obj, value) => {
|
||||||
|
let newValue = { ...value };
|
||||||
|
if (value.themeAuto) {
|
||||||
|
newValue.theme = 'auto';
|
||||||
|
}
|
||||||
|
if (value.languageAuto) {
|
||||||
|
newValue.language = 'auto';
|
||||||
|
}
|
||||||
|
for (let i = 0; i < obj.length; i++) {
|
||||||
|
let $item = $(obj[i]);
|
||||||
|
let type = $item.attr('value');
|
||||||
|
if (!newValue[type]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$item.val(newValue[type]).trigger("change");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Setting.configMenuGetValue = (obj) => {
|
||||||
|
let config = {};
|
||||||
|
for (let i = 0; i < obj.length; i++) {
|
||||||
|
let $item = $(obj[i]);
|
||||||
|
config[$item.attr('value')] = $item.val();
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
Setting.refreshUpdateMenuStatus = (localVersion, cloudVersion, needsUpdate, url) => {
|
||||||
|
const $serverDiv = $('#setting-menu-update-server');
|
||||||
|
const $btnDiv = $('#setting-menu-update > div:nth-child(2)');
|
||||||
|
const $button = $btnDiv.children('button');
|
||||||
|
$button.removeClass('layui-btn-disabled');
|
||||||
|
$button.addClass('self-adaption-btn');
|
||||||
|
const $mixlyProgress = $serverDiv.find('.mixly-progress');
|
||||||
|
$serverDiv.find('span').css('display', 'none');
|
||||||
|
$mixlyProgress.hide();
|
||||||
|
$mixlyProgress.find('.progress').show();
|
||||||
|
$mixlyProgress.removeClass('swinging indeterminate');
|
||||||
|
if (needsUpdate) {
|
||||||
|
$serverDiv.find('span.obsolete').css('display', 'inline-block');
|
||||||
|
$serverDiv.find('text').text(`${localVersion} → ${cloudVersion}`);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$serverDiv.find('span.latest').css('display', 'inline-block');
|
||||||
|
$serverDiv.find('text').text(localVersion);
|
||||||
|
}
|
||||||
|
if (needsUpdate) {
|
||||||
|
$btnDiv.css('display', 'flex');
|
||||||
|
$button.off().one('click', (event) => {
|
||||||
|
$button.addClass('layui-btn-disabled');
|
||||||
|
$button.removeClass('self-adaption-btn');
|
||||||
|
const eventSource = new EventSource(`/api/download?url=${encodeURIComponent(url)}&cloudVersion=${cloudVersion}`);
|
||||||
|
$mixlyProgress.show();
|
||||||
|
eventSource.onmessage = function(event) {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === 'progress') {
|
||||||
|
$mixlyProgress.progress({
|
||||||
|
percent: data.progress
|
||||||
|
});
|
||||||
|
} else if (data.type === 'unzip') {
|
||||||
|
$mixlyProgress.addClass('swinging indeterminate');
|
||||||
|
$mixlyProgress.progress({
|
||||||
|
percent: 100
|
||||||
|
});
|
||||||
|
$mixlyProgress.find('.progress').hide();
|
||||||
|
layer.msg('解压中...', { time: 1000 });
|
||||||
|
} else if (data.type === 'complete') {
|
||||||
|
$mixlyProgress.removeClass('swinging indeterminate');
|
||||||
|
layer.msg('更新完成!5秒后自动刷新...', { time: 1000 });
|
||||||
|
eventSource.close();
|
||||||
|
setTimeout(function(){
|
||||||
|
window.location.reload();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
eventSource.onerror = function(error) {
|
||||||
|
layer.msg('下载失败!5秒后自动刷新...', { time: 1000 });
|
||||||
|
setTimeout(function(){
|
||||||
|
window.location.reload();
|
||||||
|
}, 5000);
|
||||||
|
eventSource.close();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$btnDiv.css('display', 'none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Setting.showUpdateMessage = (data) => {
|
||||||
|
Setting.ace.updateSelectionMarkers();
|
||||||
|
const { selection, session } = Setting.ace;
|
||||||
|
const initCursor = selection.getCursor();
|
||||||
|
Setting.ace.gotoLine(session.getLength());
|
||||||
|
selection.moveCursorLineEnd();
|
||||||
|
Setting.ace.insert(data);
|
||||||
|
Setting.ace.gotoLine(session.getLength());
|
||||||
|
selection.moveCursorLineEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
Setting.createAceEditor = (container, language = 'txt', tabSize = 4) => {
|
||||||
|
let codeEditor = ace.edit(container);
|
||||||
|
if (USER.theme === 'dark') {
|
||||||
|
codeEditor.setTheme('ace/theme/dracula');
|
||||||
|
} else {
|
||||||
|
codeEditor.setTheme('ace/theme/xcode');
|
||||||
|
}
|
||||||
|
codeEditor.getSession().setMode(`ace/mode/${language}`);
|
||||||
|
codeEditor.getSession().setTabSize(tabSize);
|
||||||
|
codeEditor.setFontSize(15);
|
||||||
|
codeEditor.setShowPrintMargin(false);
|
||||||
|
codeEditor.setReadOnly(true);
|
||||||
|
codeEditor.setScrollSpeed(0.8);
|
||||||
|
codeEditor.setShowPrintMargin(false);
|
||||||
|
codeEditor.renderer.setShowGutter(false);
|
||||||
|
codeEditor.setValue('', -1);
|
||||||
|
return codeEditor;
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
247
mixly/mixly-sw/mixly-modules/common/xml.js
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
(() => {
|
||||||
|
|
||||||
|
goog.require('layui');
|
||||||
|
goog.require('Mixly.Env');
|
||||||
|
goog.require('Mixly.Config');
|
||||||
|
goog.require('Mixly.Msg');
|
||||||
|
goog.provide('Mixly.XML');
|
||||||
|
|
||||||
|
const { Env, Config, Msg, XML } = Mixly;
|
||||||
|
const { SOFTWARE, USER } = Config;
|
||||||
|
const { laytpl } = layui;
|
||||||
|
|
||||||
|
XML.TEMPLATE_DIR_PATH = './mixly-sw/templete';
|
||||||
|
|
||||||
|
let env = 'electron';
|
||||||
|
if (Env.hasSocketServer) {
|
||||||
|
env = 'web-socket';
|
||||||
|
} else if (Env.hasCompiler) {
|
||||||
|
env = 'web-compiler';
|
||||||
|
}
|
||||||
|
if (env === 'electron' && !goog.isElectron) {
|
||||||
|
env = 'web';
|
||||||
|
}
|
||||||
|
if (typeof nw === 'object') {
|
||||||
|
env = 'nw';
|
||||||
|
}
|
||||||
|
|
||||||
|
XML.TEMPLATE_CONFIG = [
|
||||||
|
{
|
||||||
|
type: 'SETTING_DIV',
|
||||||
|
path: '/setting-div.html',
|
||||||
|
config: {
|
||||||
|
env,
|
||||||
|
personalise: () => {
|
||||||
|
return Msg.getLang('PERSONAL');
|
||||||
|
},
|
||||||
|
theme: () => {
|
||||||
|
return Msg.getLang('THEME');
|
||||||
|
},
|
||||||
|
light: () => {
|
||||||
|
return Msg.getLang('LIGHT');
|
||||||
|
},
|
||||||
|
dark: () => {
|
||||||
|
return Msg.getLang('DARK');
|
||||||
|
},
|
||||||
|
language: () => {
|
||||||
|
return Msg.getLang('LANGUAGE');
|
||||||
|
},
|
||||||
|
cache: () => {
|
||||||
|
return Msg.getLang('CACHE');
|
||||||
|
},
|
||||||
|
autoUpdate: () => {
|
||||||
|
return Msg.getLang('AUTO_CHECK_UPDATE');
|
||||||
|
},
|
||||||
|
blockRenderer: () => {
|
||||||
|
return Msg.getLang('BLOCKS_RENDER');
|
||||||
|
},
|
||||||
|
apply: () => {
|
||||||
|
return Msg.getLang('APPLY');
|
||||||
|
},
|
||||||
|
reset: () => {
|
||||||
|
return Msg.getLang('RESET');
|
||||||
|
},
|
||||||
|
compileCAndH: () => {
|
||||||
|
return Msg.getLang('COMPILE_WITH_OTHERS');
|
||||||
|
},
|
||||||
|
autoOpenPort: () => {
|
||||||
|
return Msg.getLang('AUTO_OPEN_SERIAL_PORT');
|
||||||
|
},
|
||||||
|
autoWithSys: () => {
|
||||||
|
return Msg.getLang('FOLLOW_SYS');
|
||||||
|
},
|
||||||
|
yes: () => {
|
||||||
|
return Msg.getLang('ENABLE');
|
||||||
|
},
|
||||||
|
no: () => {
|
||||||
|
return Msg.getLang('DISABLE');
|
||||||
|
},
|
||||||
|
manageBoard: () => {
|
||||||
|
return Msg.getLang('MANAGE_BOARD');
|
||||||
|
},
|
||||||
|
resetBoard: () => {
|
||||||
|
return Msg.getLang('RESET_BOARD');
|
||||||
|
},
|
||||||
|
importBoard: () => {
|
||||||
|
return Msg.getLang('IMPORT_BOARD');
|
||||||
|
},
|
||||||
|
softwareSettings: () => {
|
||||||
|
return Msg.getLang('SOFTWARE');
|
||||||
|
},
|
||||||
|
boardSettings: () => {
|
||||||
|
return Msg.getLang('BOARD');
|
||||||
|
},
|
||||||
|
checkForUpdates: () => {
|
||||||
|
return Msg.getLang('UPDATE');
|
||||||
|
},
|
||||||
|
server: () => {
|
||||||
|
return Msg.getLang('SERVER');
|
||||||
|
},
|
||||||
|
client: () => {
|
||||||
|
return Msg.getLang('CLIENT');
|
||||||
|
},
|
||||||
|
version: () => {
|
||||||
|
return Msg.getLang('VERSION');
|
||||||
|
},
|
||||||
|
latest: () => {
|
||||||
|
return Msg.getLang('LATEST');
|
||||||
|
},
|
||||||
|
obsolete: () => {
|
||||||
|
return Msg.getLang('TO_BE_UPDATED');
|
||||||
|
},
|
||||||
|
update: () => {
|
||||||
|
return Msg.getLang('UPDATE');
|
||||||
|
},
|
||||||
|
experimental: () => {
|
||||||
|
return Msg.getLang('EXPERIMENTAL');
|
||||||
|
},
|
||||||
|
blocklyContentHighlight: () => {
|
||||||
|
return Msg.getLang('WORKSPACE_HIGHLIGHT');
|
||||||
|
},
|
||||||
|
blocklyShowGrid: () => {
|
||||||
|
return Msg.getLang('WORKSPACE_GRID');
|
||||||
|
},
|
||||||
|
blocklyShowMinimap: () => {
|
||||||
|
return Msg.getLang('WORKSPACE_MINIMAP');
|
||||||
|
},
|
||||||
|
blocklyMultiselect: () => {
|
||||||
|
return Msg.getLang('WORKSPACE_MULTISELECT');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
appendToBody: true,
|
||||||
|
generateDom: false,
|
||||||
|
render: true
|
||||||
|
}, {
|
||||||
|
type: 'PROGRESS_BAR_DIV',
|
||||||
|
path: '/progress-bar-div.html',
|
||||||
|
config: {},
|
||||||
|
appendToBody: false,
|
||||||
|
generateDom: false,
|
||||||
|
render: false
|
||||||
|
}, {
|
||||||
|
type: 'LOADER_DIV',
|
||||||
|
path: '/loader-div.html',
|
||||||
|
config: {
|
||||||
|
btnName: () => {
|
||||||
|
return Msg.getLang('CANCEL');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
appendToBody: true,
|
||||||
|
generateDom: false,
|
||||||
|
render: true
|
||||||
|
}, {
|
||||||
|
type: 'INTERFACE',
|
||||||
|
path: '/interface.html',
|
||||||
|
config: {},
|
||||||
|
appendToBody: false,
|
||||||
|
generateDom: false,
|
||||||
|
render: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
XML.TEMPLATE_ENV = {
|
||||||
|
SETTING_DIV: true,
|
||||||
|
PROGRESS_BAR_DIV: true,
|
||||||
|
LOADER_DIV: true,
|
||||||
|
INTERFACE: true
|
||||||
|
};
|
||||||
|
|
||||||
|
XML.TEMPLATE_STR = {};
|
||||||
|
|
||||||
|
XML.TEMPLATE_STR_RENDER = {};
|
||||||
|
|
||||||
|
XML.TEMPLATE_DOM = {};
|
||||||
|
|
||||||
|
XML.render = (xmlStr, config = {}) => {
|
||||||
|
const newConfig = {};
|
||||||
|
for (let i in config) {
|
||||||
|
if (typeof config[i] === 'function')
|
||||||
|
newConfig[i] = config[i]();
|
||||||
|
else
|
||||||
|
newConfig[i] = config[i];
|
||||||
|
}
|
||||||
|
return laytpl(xmlStr).render(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
XML.renderAllTemplete = () => {
|
||||||
|
for (let i of XML.TEMPLATE_CONFIG) {
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
config,
|
||||||
|
appendToBody,
|
||||||
|
render
|
||||||
|
} = i;
|
||||||
|
if (render && XML.TEMPLATE_ENV[type]) {
|
||||||
|
const xmlStr = XML.TEMPLATE_STR[type];
|
||||||
|
XML.TEMPLATE_STR_RENDER[type] = XML.render(xmlStr);
|
||||||
|
if (appendToBody) {
|
||||||
|
$('*[mxml-id="' + type + '"]').remove();
|
||||||
|
XML.TEMPLATE_DOM[type] = XML.getDom(xmlStr, config);
|
||||||
|
XML.TEMPLATE_DOM[type].attr('mxml-id', type);
|
||||||
|
$('body').append(XML.TEMPLATE_DOM[type]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
XML.getDom = (xmlStr, config = {}) => {
|
||||||
|
return $(XML.render(xmlStr, config));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i of XML.TEMPLATE_CONFIG) {
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
path,
|
||||||
|
config,
|
||||||
|
appendToBody,
|
||||||
|
generateDom
|
||||||
|
} = i;
|
||||||
|
if (XML.TEMPLATE_ENV[type]) {
|
||||||
|
const xmlStr = goog.readFileSync(XML.TEMPLATE_DIR_PATH + path);
|
||||||
|
if (xmlStr) {
|
||||||
|
XML.TEMPLATE_STR[type] = xmlStr;
|
||||||
|
if (generateDom) {
|
||||||
|
XML.TEMPLATE_STR_RENDER[type] = XML.render(xmlStr, config);
|
||||||
|
XML.TEMPLATE_DOM[type] = XML.getDom(xmlStr, config);
|
||||||
|
}
|
||||||
|
if (appendToBody) {
|
||||||
|
if (!XML.TEMPLATE_DOM[type]) {
|
||||||
|
XML.TEMPLATE_DOM[type] = XML.getDom(xmlStr, config);
|
||||||
|
}
|
||||||
|
XML.TEMPLATE_DOM[type].attr('mxml-id', type);
|
||||||
|
$('body').append(XML.TEMPLATE_DOM[type]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
for (let i of XML.TEMPLATE_CONFIG) {
|
||||||
|
const { type, appendToBody } = i;
|
||||||
|
if (XML.TEMPLATE_ENV[type] && XML.TEMPLATE_DOM[type] && appendToBody) {
|
||||||
|
$('body').append(XML.TEMPLATE_DOM[type]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
143
mixly/mixly-sw/mixly-modules/deps.json
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"path": "/common/board-manager.js",
|
||||||
|
"require": [
|
||||||
|
"path",
|
||||||
|
"layui",
|
||||||
|
"Mixly.Env",
|
||||||
|
"Mixly.Msg",
|
||||||
|
"Mixly.XML",
|
||||||
|
"Mixly.LayerExt",
|
||||||
|
"Mixly.Config",
|
||||||
|
"Mixly.MArray",
|
||||||
|
"Mixly.Url",
|
||||||
|
"Mixly.Storage",
|
||||||
|
"Mixly.Electron.CloudDownload",
|
||||||
|
"Mixly.Electron.PythonShell"
|
||||||
|
],
|
||||||
|
"provide": [
|
||||||
|
"Mixly.BoardManager"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/common/config.js",
|
||||||
|
"require": [
|
||||||
|
"Mixly.Url",
|
||||||
|
"Mixly.LocalStorage"
|
||||||
|
],
|
||||||
|
"provide": [
|
||||||
|
"Mixly.Config"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/common/env.js",
|
||||||
|
"require": [
|
||||||
|
"path",
|
||||||
|
"Mixly",
|
||||||
|
"Mixly.Config"
|
||||||
|
],
|
||||||
|
"provide": [
|
||||||
|
"Mixly.Env"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/common/events.js",
|
||||||
|
"require": [
|
||||||
|
"Mixly.BoardManager",
|
||||||
|
"Mixly.Env",
|
||||||
|
"Mixly.Config",
|
||||||
|
"Mixly.Url"
|
||||||
|
],
|
||||||
|
"provide": [
|
||||||
|
"Mixly.Events"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/common/loader.js",
|
||||||
|
"require": [
|
||||||
|
"layui",
|
||||||
|
"Mixly.Url",
|
||||||
|
"Mixly.Env",
|
||||||
|
"Mixly.Config",
|
||||||
|
"Mixly.BoardManager",
|
||||||
|
"Mixly.XML",
|
||||||
|
"Mixly.Msg",
|
||||||
|
"Mixly.Setting",
|
||||||
|
"Mixly.Events",
|
||||||
|
"Mixly.Electron.PythonShell",
|
||||||
|
"Mixly.WebSocket.Socket"
|
||||||
|
],
|
||||||
|
"provide": [
|
||||||
|
"Mixly.Loader"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/common/msg.js",
|
||||||
|
"require": [
|
||||||
|
"Mixly.MJson",
|
||||||
|
"Mixly.Config"
|
||||||
|
],
|
||||||
|
"provide": [
|
||||||
|
"Mixly.Msg"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/common/setting.js",
|
||||||
|
"require": [
|
||||||
|
"ace",
|
||||||
|
"ace.ExtLanguageTools",
|
||||||
|
"layui",
|
||||||
|
"store",
|
||||||
|
"$.select2",
|
||||||
|
"$.fomanticUI",
|
||||||
|
"Mixly.XML",
|
||||||
|
"Mixly.LayerExt",
|
||||||
|
"Mixly.Msg",
|
||||||
|
"Mixly.BoardManager",
|
||||||
|
"Mixly.Config",
|
||||||
|
"Mixly.Env",
|
||||||
|
"Mixly.MJson",
|
||||||
|
"Mixly.Storage",
|
||||||
|
"Mixly.WebSocket.Socket"
|
||||||
|
],
|
||||||
|
"provide": [
|
||||||
|
"Mixly.Setting"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/common/xml.js",
|
||||||
|
"require": [
|
||||||
|
"layui",
|
||||||
|
"Mixly.Env",
|
||||||
|
"Mixly.Config",
|
||||||
|
"Mixly.Msg"
|
||||||
|
],
|
||||||
|
"provide": [
|
||||||
|
"Mixly.XML"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/electron/python-shell.js",
|
||||||
|
"require": [
|
||||||
|
"Mixly.Env",
|
||||||
|
"Mixly.Electron"
|
||||||
|
],
|
||||||
|
"provide": [
|
||||||
|
"Mixly.Electron.PythonShell"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/web-socket/socket.js",
|
||||||
|
"require": [
|
||||||
|
"Mixly.Env",
|
||||||
|
"Mixly.Config",
|
||||||
|
"Mixly.MJson",
|
||||||
|
"Mixly.WebSocket",
|
||||||
|
"Mixly.LayerExt",
|
||||||
|
"Mixly.Command"
|
||||||
|
],
|
||||||
|
"provide": [
|
||||||
|
"Mixly.WebSocket.Socket"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
39
mixly/mixly-sw/mixly-modules/electron/python-shell.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
(() => {
|
||||||
|
|
||||||
|
goog.require('Mixly.Env');
|
||||||
|
goog.require('Mixly.Electron');
|
||||||
|
goog.provide('Mixly.Electron.PythonShell');
|
||||||
|
|
||||||
|
const {
|
||||||
|
Env,
|
||||||
|
Electron
|
||||||
|
} = Mixly;
|
||||||
|
|
||||||
|
const fs_extra = Mixly.require('fs-extra');
|
||||||
|
const fs_plus = Mixly.require('fs-plus');
|
||||||
|
const python_shell = Mixly.require('python-shell');
|
||||||
|
|
||||||
|
const { PythonShell } = Electron;
|
||||||
|
|
||||||
|
PythonShell.init = () => {
|
||||||
|
if (Env.currentPlatform !== 'win32' && fs_plus.isFileSync('/usr/local/bin/python3')) {
|
||||||
|
Env.python3Path = '/usr/local/bin/python3';
|
||||||
|
}
|
||||||
|
PythonShell.OPTIONS = {
|
||||||
|
pythonPath: Env.python3Path,
|
||||||
|
pythonOptions: ['-u'],
|
||||||
|
encoding: "binary",
|
||||||
|
mode: 'utf-8'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
PythonShell.run = (indexPath, pyFilePath) => {
|
||||||
|
indexPath = decodeURIComponent(indexPath);
|
||||||
|
pyFilePath = decodeURIComponent(pyFilePath);
|
||||||
|
const shell = new python_shell.PythonShell(pyFilePath, {
|
||||||
|
...PythonShell.OPTIONS,
|
||||||
|
args: [ Env.clientPath, indexPath ]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
244
mixly/mixly-sw/mixly-modules/web-socket/socket.js
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
(() => {
|
||||||
|
|
||||||
|
goog.require('Mixly.Env');
|
||||||
|
goog.require('Mixly.Config');
|
||||||
|
goog.require('Mixly.MJson');
|
||||||
|
goog.require('Mixly.WebSocket');
|
||||||
|
goog.require('Mixly.LayerExt');
|
||||||
|
goog.require('Mixly.Command');
|
||||||
|
goog.provide('Mixly.WebSocket.Socket');
|
||||||
|
|
||||||
|
const {
|
||||||
|
Env,
|
||||||
|
Config,
|
||||||
|
MJson,
|
||||||
|
LayerExt,
|
||||||
|
Command
|
||||||
|
} = Mixly;
|
||||||
|
|
||||||
|
const { SOFTWARE } = Config;
|
||||||
|
|
||||||
|
const { Socket } = Mixly.WebSocket;
|
||||||
|
|
||||||
|
Socket.obj = null;
|
||||||
|
Socket.url = 'ws://127.0.0.1/socket';
|
||||||
|
Socket.jsonArr = [];
|
||||||
|
Socket.connected = false;
|
||||||
|
Socket.initFunc = null;
|
||||||
|
Socket.debug = SOFTWARE.debug;
|
||||||
|
let { hostname, protocol, port } = window.location;
|
||||||
|
if (protocol === 'http:') {
|
||||||
|
Socket.protocol = 'ws:';
|
||||||
|
} else {
|
||||||
|
Socket.protocol = 'wss:';
|
||||||
|
}
|
||||||
|
if (port) {
|
||||||
|
port = ':' + port;
|
||||||
|
}
|
||||||
|
Socket.url = Socket.protocol + '//' + hostname + port + '/socket';
|
||||||
|
Socket.IPAddress = hostname;
|
||||||
|
Socket.disconnectTimes = 0;
|
||||||
|
Socket.updating = false;
|
||||||
|
|
||||||
|
|
||||||
|
let lockReconnect = false; // 避免重复连接
|
||||||
|
let timeoutFlag = true;
|
||||||
|
let timeoutSet = null;
|
||||||
|
let reconectNum = 0;
|
||||||
|
const timeout = 5000; // 超时重连间隔
|
||||||
|
|
||||||
|
function reconnect () {
|
||||||
|
if (lockReconnect) return;
|
||||||
|
lockReconnect = true;
|
||||||
|
// 没连接上会一直重连,设置延迟避免请求过多
|
||||||
|
setTimeout(function () {
|
||||||
|
timeoutFlag = true;
|
||||||
|
Socket.init();
|
||||||
|
console.info(`正在重连第${reconectNum + 1}次`);
|
||||||
|
reconectNum++;
|
||||||
|
lockReconnect = false;
|
||||||
|
}, timeout); // 这里设置重连间隔(ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
//心跳检测
|
||||||
|
const heartCheck = {
|
||||||
|
timeout, // 毫秒
|
||||||
|
timeoutObj: null,
|
||||||
|
serverTimeoutObj: null,
|
||||||
|
reset: function () {
|
||||||
|
clearInterval(this.timeoutObj);
|
||||||
|
clearTimeout(this.serverTimeoutObj);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
start: function () {
|
||||||
|
const self = this;
|
||||||
|
let count = 0;
|
||||||
|
let WS = Socket;
|
||||||
|
this.timeoutObj = setInterval(() => {
|
||||||
|
if (count < 3) {
|
||||||
|
if (WS.obj.readyState === 1) {
|
||||||
|
WS.obj.send('HeartBeat');
|
||||||
|
console.info(`HeartBeat第${count + 1}次`);
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
} else {
|
||||||
|
clearInterval(this.timeoutObj);
|
||||||
|
count = 0;
|
||||||
|
if (WS.obj.readyState === 0 && WS.obj.readyState === 1) {
|
||||||
|
WS.obj.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, self.timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Socket.init = (onopenFunc = (data) => {}, doFunc = () => {}) => {
|
||||||
|
if (Socket.connected) {
|
||||||
|
if (Socket.initFunc) {
|
||||||
|
Socket.initFunc();
|
||||||
|
Socket.initFunc = null;
|
||||||
|
}
|
||||||
|
doFunc();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutSet = setTimeout(() => {
|
||||||
|
if (timeoutFlag && reconectNum < 3) {
|
||||||
|
console.info(`重连`);
|
||||||
|
reconectNum++;
|
||||||
|
Socket.init();
|
||||||
|
}
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
let WS = Socket;
|
||||||
|
WS.obj = new WebSocket(WS.url);
|
||||||
|
WS.obj.onopen = () => {
|
||||||
|
console.log('已连接' + WS.url);
|
||||||
|
WS.connected = true;
|
||||||
|
Socket.initFunc = doFunc;
|
||||||
|
reconectNum = 0;
|
||||||
|
timeoutFlag = false;
|
||||||
|
clearTimeout(timeoutSet);
|
||||||
|
heartCheck.reset().start();
|
||||||
|
onopenFunc(WS);
|
||||||
|
Socket.reload();
|
||||||
|
if (Socket.updating) {
|
||||||
|
Socket.updating = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
WS.obj.onmessage = (event) => {
|
||||||
|
heartCheck.reset().start();
|
||||||
|
let command = Command.parse(event.data);
|
||||||
|
command = MJson.decode(command);
|
||||||
|
if (Socket.debug)
|
||||||
|
console.log('receive -> ', event.data);
|
||||||
|
Command.run(command);
|
||||||
|
};
|
||||||
|
|
||||||
|
WS.obj.onerror = (event) => {
|
||||||
|
console.log('WebSocket error: ', event);
|
||||||
|
reconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
WS.obj.onclose = (event) => {
|
||||||
|
WS.connected = false;
|
||||||
|
WS.disconnectTimes += 1;
|
||||||
|
if (WS.disconnectTimes > 255) {
|
||||||
|
WS.disconnectTimes = 1;
|
||||||
|
}
|
||||||
|
console.log('已断开' + WS.url);
|
||||||
|
|
||||||
|
console.info(`关闭`, event.code);
|
||||||
|
if (event.code !== 1000) {
|
||||||
|
timeoutFlag = false;
|
||||||
|
clearTimeout(timeoutSet);
|
||||||
|
reconnect();
|
||||||
|
} else {
|
||||||
|
clearInterval(heartCheck.timeoutObj);
|
||||||
|
clearTimeout(heartCheck.serverTimeoutObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Socket.sendCommand = (command) => {
|
||||||
|
let WS = Mixly.WebSocket.Socket;
|
||||||
|
if (!WS.connected) {
|
||||||
|
layer.msg('未连接' + WS.url, {time: 1000});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let commandStr = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
commandStr = JSON.stringify(MJson.encode(command));
|
||||||
|
if (Socket.debug)
|
||||||
|
console.log('send -> ', commandStr);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WS.obj.send(commandStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
Socket.clickConnect = () => {
|
||||||
|
if (Socket.connected) {
|
||||||
|
Socket.disconnect();
|
||||||
|
} else {
|
||||||
|
Socket.connect((WS) => {
|
||||||
|
layer.closeAll();
|
||||||
|
layer.msg(WS.url + '连接成功', { time: 1000 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Socket.openLoadingBox = (title, successFunc = () => {}, endFunc = () => {}) => {
|
||||||
|
layer.open({
|
||||||
|
type: 1,
|
||||||
|
title: title,
|
||||||
|
content: $('#mixly-loader-div'),
|
||||||
|
shade: LayerExt.SHADE_ALL,
|
||||||
|
closeBtn: 0,
|
||||||
|
success: function () {
|
||||||
|
$("#webusb-cancel").css("display","none");
|
||||||
|
$(".layui-layer-page").css("z-index", "198910151");
|
||||||
|
successFunc();
|
||||||
|
},
|
||||||
|
end: function () {
|
||||||
|
$("#mixly-loader-div").css("display", "none");
|
||||||
|
$(".layui-layer-shade").remove();
|
||||||
|
$("#webusb-cancel").css("display", "unset");
|
||||||
|
if (Socket.connected)
|
||||||
|
endFunc();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Socket.connect = (onopenFunc = (data) => {}, doFunc = () => {}) => {
|
||||||
|
if (Socket.connected) {
|
||||||
|
doFunc();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let title = '连接中...';
|
||||||
|
Socket.openLoadingBox(title, () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
Socket.init(onopenFunc);
|
||||||
|
}, 1000);
|
||||||
|
}, doFunc);
|
||||||
|
}
|
||||||
|
|
||||||
|
Socket.disconnect = () => {
|
||||||
|
if (!Socket.connected)
|
||||||
|
return;
|
||||||
|
let title = '断开中...';
|
||||||
|
Socket.openLoadingBox(title, () => {
|
||||||
|
Socket.obj.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Socket.reload = () => {
|
||||||
|
if (!Socket.updating && Socket.disconnectTimes) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
84
mixly/mixly-sw/msg/en.json
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"SETTING": "Setting",
|
||||||
|
"PROGRESS": "Progress",
|
||||||
|
"NAME": "Name",
|
||||||
|
"VERSION": "Version",
|
||||||
|
"INTRODUCTION": "introduction",
|
||||||
|
"CLOUD_IMPORT": "Cloud import",
|
||||||
|
"LOCAL_IMPORT": "Local import",
|
||||||
|
"CLOUD_BOARD": "Cloud board",
|
||||||
|
"IMPORT_BOARD": "Import board",
|
||||||
|
"SELECT_AT_LEAST_ONE_CLOUD_BOARD": "Please select at least one cloud card",
|
||||||
|
"UNZIPPING": "Unzipping",
|
||||||
|
"IMPORT_SUCC": "Import successful",
|
||||||
|
"IMPORT_FAILED": "Import failed",
|
||||||
|
"CANCEL": "Cancel",
|
||||||
|
"PERSONAL": "Personalise",
|
||||||
|
"THEME": "Theme",
|
||||||
|
"LIGHT": "Light",
|
||||||
|
"DARK": "Dark",
|
||||||
|
"LANGUAGE": "Language",
|
||||||
|
"BLOCKS_RENDER": "Block renderer",
|
||||||
|
"APPLY": "Apply",
|
||||||
|
"RESET": "Reset",
|
||||||
|
"CONFIG_UPDATE_SUCC": "Configuration updated successfully",
|
||||||
|
"CONFIRM": "Confirm",
|
||||||
|
"IMPORTING_BOARD": "Importing the board",
|
||||||
|
"DELETING_BOARD": "Deleting the board",
|
||||||
|
"BOARD_IMPORTED": "This board has been imported",
|
||||||
|
"SELECT_CONFIG_FILE_ERR": "The selected file is not a configuration file",
|
||||||
|
"FILE_NOT_EXIST": "File does not exist",
|
||||||
|
"CONFIG_FILE_DECODE_ERR": "Configuration file parsing failed",
|
||||||
|
"IMPORT_COMPLETE": "Import Complete",
|
||||||
|
"BOARD_URL_READ_ERR": "Error reading board url",
|
||||||
|
"BOARD_FILE_DOWNLOADING": "Downloading board file",
|
||||||
|
"BOARD_FILE_DOWNLOAD_COMPLETE": "Board file download complete",
|
||||||
|
"BOARD_FILE_DOWNLOAD_FAILED": "Failed to download board file",
|
||||||
|
"BOARD_FILE_UNZIPPING": "Unzipping the board file",
|
||||||
|
"BOARD_FILE_UNZIP_COMPLETE": "Board file decompression completed",
|
||||||
|
"BOARD_FILE_UNZIP_FAILED": "Failed to decompress the board file",
|
||||||
|
"BOARD_PACKAGE_INDEX_DOWNLOADING": "Downloading board package index",
|
||||||
|
"BOARD_PACKAGE_INDEX_DOWNLOAD_COMPLETE": "Board package index download complete",
|
||||||
|
"BOARD_PACKAGE_INDEX_DOWNLOAD_FAILED": "Failed to download the board package index",
|
||||||
|
"BOARD_PACKAGE_DOWNLOADING": "Downloading board package",
|
||||||
|
"BOARD_PACKAGE_DOWNLOAD_COMPLETE": "Board package download complete",
|
||||||
|
"BOARD_PACKAGE_DOWNLOAD_FAILED": "Failed to download board package",
|
||||||
|
"BOARD_PACKAGE_UNZIPPING": "The board package is being decompressed",
|
||||||
|
"BOARD_PACKAGE_UNZIP_COMPLETE": "Board package decompression complete",
|
||||||
|
"BOARD_PACKAGE_UNZIP_FAILED": "Failed to decompress the board package",
|
||||||
|
"ALREADY_THE_LATEST_VERSION": "Already the latest version",
|
||||||
|
"DOWNLOADING": "Downloading",
|
||||||
|
"DOWNLOAD_COMPLETE": "Download Completed",
|
||||||
|
"DOWNLOAD_FAILED": "Download failed",
|
||||||
|
"UNZIP_COMPLETE": "Decompression complete",
|
||||||
|
"UNZIP_FAILED": "Decompression failed",
|
||||||
|
"TO_BE_UPDATED": "To be updated",
|
||||||
|
"INSTALLED": "Installed",
|
||||||
|
"TO_BE_INSTALLED": "To be installed",
|
||||||
|
"STATUS": "Status",
|
||||||
|
"CLOUD_BOARD_JSON_DOWNLOADING": "Cloud board JSON downloading",
|
||||||
|
"CLOUD_BOARD_JSON_DOWNLOAD_FAILED": "Cloud board JSON download failed",
|
||||||
|
"COMPILE_WITH_OTHERS": "Compile .c and .h",
|
||||||
|
"RESET_BOARD": "Reset board",
|
||||||
|
"MANAGE_BOARD": "Manage board",
|
||||||
|
"AUTO_CHECK_UPDATE": "Automatically check for updates",
|
||||||
|
"AUTO_OPEN_SERIAL_PORT": "Auto open the SerialPort after uploading",
|
||||||
|
"SOFTWARE": "Software",
|
||||||
|
"BOARD": "Board",
|
||||||
|
"FOLLOW_SYS": "Follow the system",
|
||||||
|
"UPDATE": "Update",
|
||||||
|
"SERVER": "Server",
|
||||||
|
"CLIENT": "Client",
|
||||||
|
"LATEST": "latest",
|
||||||
|
"UPDATE": "Update",
|
||||||
|
"INFO": "The page will be automatically reloaded after the update is in progress.<br/>if the page has not been reloaded for a long time,<br/>try to reload the page manually.",
|
||||||
|
"WORKSPACE_HIGHLIGHT": "Workspace highlight",
|
||||||
|
"WORKSPACE_GRID": "Workspace grid",
|
||||||
|
"WORKSPACE_MINIMAP": "Workspace minimap",
|
||||||
|
"WORKSPACE_MULTISELECT": "Workspace multiselect",
|
||||||
|
"CODE_LANG": "Code language",
|
||||||
|
"ENABLE": "Enable",
|
||||||
|
"DISABLE": "Disable",
|
||||||
|
"EXPERIMENTAL": "Experimental. Expected behavior may change in the future.",
|
||||||
|
"CACHE": "Automatically restore the last editing state"
|
||||||
|
}
|
||||||
84
mixly/mixly-sw/msg/zh-hans.json
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"SETTING": "设置",
|
||||||
|
"PROGRESS": "进度",
|
||||||
|
"NAME": "名称",
|
||||||
|
"VERSION": "版本",
|
||||||
|
"INTRODUCTION": "介绍",
|
||||||
|
"CLOUD_IMPORT": "云端导入",
|
||||||
|
"LOCAL_IMPORT": "本地导入",
|
||||||
|
"CLOUD_BOARD": "云端板卡",
|
||||||
|
"IMPORT_BOARD": "导入板卡",
|
||||||
|
"SELECT_AT_LEAST_ONE_CLOUD_BOARD": "请至少选择一块云端板卡",
|
||||||
|
"UNZIPPING": "解压中",
|
||||||
|
"IMPORT_SUCC": "导入成功",
|
||||||
|
"IMPORT_FAILED": "导入失败",
|
||||||
|
"CANCEL": "取消",
|
||||||
|
"PERSONAL": "个性化",
|
||||||
|
"THEME": "主题",
|
||||||
|
"LIGHT": "浅色",
|
||||||
|
"DARK": "深色",
|
||||||
|
"LANGUAGE": "语言",
|
||||||
|
"BLOCKS_RENDER": "渲染器",
|
||||||
|
"APPLY": "应用",
|
||||||
|
"RESET": "复位",
|
||||||
|
"CONFIG_UPDATE_SUCC": "配置更新成功",
|
||||||
|
"CONFIRM": "CONFIRM",
|
||||||
|
"IMPORTING_BOARD": "板卡导入中",
|
||||||
|
"DELETING_BOARD": "板卡删除中",
|
||||||
|
"BOARD_IMPORTED": "此板卡已导入",
|
||||||
|
"SELECT_CONFIG_FILE_ERR": "所选文件非配置文件",
|
||||||
|
"FILE_NOT_EXIST": "文件不存在",
|
||||||
|
"CONFIG_FILE_DECODE_ERR": "配置文件解析失败",
|
||||||
|
"IMPORT_COMPLETE": "导入完成",
|
||||||
|
"BOARD_URL_READ_ERR": "板卡URL读取出错",
|
||||||
|
"BOARD_FILE_DOWNLOADING": "板卡文件下载中",
|
||||||
|
"BOARD_FILE_DOWNLOAD_COMPLETE": "板卡文件下载完成",
|
||||||
|
"BOARD_FILE_DOWNLOAD_FAILED": "板卡文件下载失败",
|
||||||
|
"BOARD_FILE_UNZIPPING": "板卡文件解压中",
|
||||||
|
"BOARD_FILE_UNZIP_COMPLETE": "板卡文件解压完成",
|
||||||
|
"BOARD_FILE_UNZIP_FAILED": "板卡文件解压失败",
|
||||||
|
"BOARD_PACKAGE_INDEX_DOWNLOADING": "板卡包索引下载中",
|
||||||
|
"BOARD_PACKAGE_INDEX_DOWNLOAD_COMPLETE": "板卡包索引下载完成",
|
||||||
|
"BOARD_PACKAGE_INDEX_DOWNLOAD_FAILED": "板卡包索引下载失败",
|
||||||
|
"BOARD_PACKAGE_DOWNLOADING": "板卡包下载中",
|
||||||
|
"BOARD_PACKAGE_DOWNLOAD_COMPLETE": "板卡包下载完成",
|
||||||
|
"BOARD_PACKAGE_DOWNLOAD_FAILED": "板卡包下载失败",
|
||||||
|
"BOARD_PACKAGE_UNZIPPING": "板卡包解压中",
|
||||||
|
"BOARD_PACKAGE_UNZIP_COMPLETE": "板卡包解压完成",
|
||||||
|
"BOARD_PACKAGE_UNZIP_FAILED": "板卡包解压失败",
|
||||||
|
"ALREADY_THE_LATEST_VERSION": "已是最新版",
|
||||||
|
"DOWNLOADING": "下载中",
|
||||||
|
"DOWNLOAD_COMPLETE": "下载完成",
|
||||||
|
"DOWNLOAD_FAILED": "下载失败",
|
||||||
|
"UNZIP_COMPLETE": "解压完成",
|
||||||
|
"UNZIP_FAILED": "解压失败",
|
||||||
|
"TO_BE_UPDATED": "待更新",
|
||||||
|
"INSTALLED": "已安装",
|
||||||
|
"TO_BE_INSTALLED": "待安装",
|
||||||
|
"STATUS": "状态",
|
||||||
|
"CLOUD_BOARD_JSON_DOWNLOADING": "云端板卡JSON下载中",
|
||||||
|
"CLOUD_BOARD_JSON_DOWNLOAD_FAILED": "云端板卡JSON下载失败",
|
||||||
|
"COMPILE_WITH_OTHERS": "同时编译同目录.c和.h",
|
||||||
|
"RESET_BOARD": "复位板卡",
|
||||||
|
"MANAGE_BOARD": "管理板卡",
|
||||||
|
"AUTO_CHECK_UPDATE": "自动检查更新",
|
||||||
|
"AUTO_OPEN_SERIAL_PORT": "上传结束后自动打开串口",
|
||||||
|
"SOFTWARE": "软件",
|
||||||
|
"BOARD": "板卡",
|
||||||
|
"FOLLOW_SYS": "跟随系统",
|
||||||
|
"UPDATE": "检查更新",
|
||||||
|
"SERVER": "服务端",
|
||||||
|
"CLIENT": "客户端",
|
||||||
|
"LATEST": "已最新",
|
||||||
|
"UPDATE": "更新",
|
||||||
|
"INFO": "正在更新中,更新结束后将会自动重载页面,<br/>若页面长时间未重载请尝试手动重载页面。",
|
||||||
|
"WORKSPACE_HIGHLIGHT": "工作区高亮",
|
||||||
|
"WORKSPACE_GRID": "工作区网格",
|
||||||
|
"WORKSPACE_MINIMAP": "工作区缩略图",
|
||||||
|
"WORKSPACE_MULTISELECT": "工作区多重选择",
|
||||||
|
"CODE_LANG": "编程语言",
|
||||||
|
"ENABLE": "开启",
|
||||||
|
"DISABLE": "关闭",
|
||||||
|
"EXPERIMENTAL": "实验性。预期行为可能会在未来发生变更。",
|
||||||
|
"CACHE": "自动恢复上次编辑状态"
|
||||||
|
}
|
||||||
84
mixly/mixly-sw/msg/zh-hant.json
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"SETTING": "設置",
|
||||||
|
"PROGRESS": "進度",
|
||||||
|
"NAME": "名稱",
|
||||||
|
"VERSION": "版本",
|
||||||
|
"INTRODUCTION": "介紹",
|
||||||
|
"CLOUD_IMPORT": "雲端導入",
|
||||||
|
"LOCAL_IMPORT": "本地導入",
|
||||||
|
"CLOUD_BOARD": "雲端闆卡",
|
||||||
|
"IMPORT_BOARD": "導入闆卡",
|
||||||
|
"SELECT_AT_LEAST_ONE_CLOUD_BOARD": "請選擇至少一塊雲端闆卡",
|
||||||
|
"UNZIPPING": "解壓中",
|
||||||
|
"IMPORT_SUCC": "導入成功",
|
||||||
|
"IMPORT_FAILED": "導入失敗",
|
||||||
|
"CANCEL": "取消",
|
||||||
|
"PERSONAL": "個性化",
|
||||||
|
"THEME": "主題",
|
||||||
|
"LIGHT": "淺色",
|
||||||
|
"DARK": "深色",
|
||||||
|
"LANGUAGE": "語言",
|
||||||
|
"BLOCKS_RENDER": "塊渲染器",
|
||||||
|
"APPLY": "應用",
|
||||||
|
"RESET": "复位",
|
||||||
|
"CONFIG_UPDATE_SUCC": "配置更新成功",
|
||||||
|
"CONFIRM": "確認",
|
||||||
|
"IMPORTING_BOARD": "闆卡導入中",
|
||||||
|
"DELETING_BOARD": "闆卡刪除中",
|
||||||
|
"BOARD_IMPORTED": "此闆卡已導入",
|
||||||
|
"SELECT_CONFIG_FILE_ERR": "所選文件非配置文件",
|
||||||
|
"FILE_NOT_EXIST": "文件不存在",
|
||||||
|
"CONFIG_FILE_DECODE_ERR": "配置文件解析失敗",
|
||||||
|
"IMPORT_COMPLETE": "導入完成",
|
||||||
|
"BOARD_URL_READ_ERR": "闆卡URL讀取出錯",
|
||||||
|
"BOARD_FILE_DOWNLOADING": "闆卡文件下載中",
|
||||||
|
"BOARD_FILE_DOWNLOAD_COMPLETE": "闆卡文件下載完成",
|
||||||
|
"BOARD_FILE_DOWNLOAD_FAILED": "闆卡文件下載失敗",
|
||||||
|
"BOARD_FILE_UNZIPPING": "闆卡文件解壓中",
|
||||||
|
"BOARD_FILE_UNZIP_COMPLETE": "闆卡文件解壓完成",
|
||||||
|
"BOARD_FILE_UNZIP_FAILED": "闆卡文件解壓失敗",
|
||||||
|
"BOARD_PACKAGE_INDEX_DOWNLOADING": "闆卡包索引下載中",
|
||||||
|
"BOARD_PACKAGE_INDEX_DOWNLOAD_COMPLETE": "闆卡包索引下載完成",
|
||||||
|
"BOARD_PACKAGE_INDEX_DOWNLOAD_FAILED": "闆卡包索引下載失敗",
|
||||||
|
"BOARD_PACKAGE_DOWNLOADING": "闆卡包下載中",
|
||||||
|
"BOARD_PACKAGE_DOWNLOAD_COMPLETE": "闆卡包下載完成",
|
||||||
|
"BOARD_PACKAGE_DOWNLOAD_FAILED": "闆卡包下載失敗",
|
||||||
|
"BOARD_PACKAGE_UNZIPPING": "闆卡包解壓中",
|
||||||
|
"BOARD_PACKAGE_UNZIP_COMPLETE": "闆卡包解壓完成",
|
||||||
|
"BOARD_PACKAGE_UNZIP_FAILED": "闆卡包解壓失敗",
|
||||||
|
"ALREADY_THE_LATEST_VERSION": "已是最新版",
|
||||||
|
"DOWNLOADING": "下载中",
|
||||||
|
"DOWNLOAD_COMPLETE": "下載完成",
|
||||||
|
"DOWNLOAD_FAILED": "下載失敗",
|
||||||
|
"UNZIP_COMPLETE": "解壓完成",
|
||||||
|
"UNZIP_FAILED": "解壓失敗",
|
||||||
|
"TO_BE_UPDATED": "已安装",
|
||||||
|
"INSTALLED": "已安裝",
|
||||||
|
"TO_BE_INSTALLED": "待安裝",
|
||||||
|
"STATUS": "狀態",
|
||||||
|
"CLOUD_BOARD_JSON_DOWNLOADING": "雲端闆卡JSON下載中",
|
||||||
|
"CLOUD_BOARD_JSON_DOWNLOAD_FAILED": "雲端闆卡JSON下載失敗",
|
||||||
|
"COMPILE_WITH_OTHERS": "同時編譯同目錄.c和.h",
|
||||||
|
"RESET_BOARD": "復位闆卡",
|
||||||
|
"MANAGE_BOARD": "管理闆卡",
|
||||||
|
"AUTO_CHECK_UPDATE": "自動檢查更新",
|
||||||
|
"AUTO_OPEN_SERIAL_PORT": "上傳結束後自動打開串口",
|
||||||
|
"SOFTWARE": "軟件",
|
||||||
|
"BOARD": "板卡",
|
||||||
|
"FOLLOW_SYS": "跟隨系統",
|
||||||
|
"UPDATE": "檢查更新",
|
||||||
|
"SERVER": "服務器",
|
||||||
|
"CLIENT": "用戶端",
|
||||||
|
"LATEST": "已最新",
|
||||||
|
"UPDATE": "更新",
|
||||||
|
"INFO": "正在更新中,更新結束後將會自動重載頁面,<br/>若頁面長時間未重載請嘗試手動重載頁面。",
|
||||||
|
"WORKSPACE_HIGHLIGHT": "工作區高亮",
|
||||||
|
"WORKSPACE_GRID": "工作區網格",
|
||||||
|
"WORKSPACE_MINIMAP": "工作區縮略圖",
|
||||||
|
"WORKSPACE_MULTISELECT": "工作區多重選擇",
|
||||||
|
"CODE_LANG": "程式碼語言",
|
||||||
|
"ENABLE": "開啟",
|
||||||
|
"DISABLE": "關閉",
|
||||||
|
"EXPERIMENTAL": "實驗性。 預期行為可能會在未來發生變更。",
|
||||||
|
"CACHE": "自動恢復上次編輯狀態"
|
||||||
|
}
|
||||||
143
mixly/mixly-sw/templete/interface.html
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue_icons {
|
||||||
|
filter: invert(13%) sepia(24%) saturate(1450%) hue-rotate(207deg) brightness(93%) contrast(105%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-area {
|
||||||
|
height: 390px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
-moz-user-select: none;
|
||||||
|
-o-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(window).scroll(function () {
|
||||||
|
if ($(window).scrollTop() >= 50) {
|
||||||
|
$('.header-area').css('background', '#1B173D');
|
||||||
|
} else {
|
||||||
|
$('.header-area').css('background', 'transparent');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<section class="slider-area" id="home">
|
||||||
|
<div class="container">
|
||||||
|
<div class="col-md-6 col-sm-6 hidden-xs">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||||
|
<div class="row">
|
||||||
|
<div class="slider-inner text-right">
|
||||||
|
<b>
|
||||||
|
<h2>Mixly</h2>
|
||||||
|
</b>
|
||||||
|
<h5>Make Programming Easier</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<style>
|
||||||
|
/*#add-board:hover {
|
||||||
|
-webkit-transform: rotate(0deg) scale(1.2) !important;
|
||||||
|
transform: rotate(1deg) scale(1.2) !important;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
.setting-card>div:nth-child(1) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-card>div:nth-child(2) {
|
||||||
|
opacity: 1;
|
||||||
|
position: absolute;
|
||||||
|
left: 15px;
|
||||||
|
right: 15px;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
padding-left: 15px;
|
||||||
|
padding-right: 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 0 8px rgb(0 0 0 / 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] .setting-card>div:nth-child(2) {
|
||||||
|
background-color: #2d2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] .setting-card>div:nth-child(2) {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-card>div:nth-child(2):hover {
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: 0 0 15px rgb(0 0 0 / 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-card>div:nth-child(2)>div {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-card>div:nth-child(2)>div>form {
|
||||||
|
width: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-card>div:nth-child(2) .layui-form-switch {
|
||||||
|
height: 24px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-card>div:nth-child(2) .layui-form-onswitch {
|
||||||
|
border-color: #686768;
|
||||||
|
background-color: #3f3e40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-card>div:nth-child(2) button {
|
||||||
|
width: 50px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-single img:hover {
|
||||||
|
-webkit-transform: rotate(0deg) scale(1.025);
|
||||||
|
transform: rotate(-3deg) scale(1.025);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] .service-single:hover {
|
||||||
|
box-shadow: 0 0 15px rgb(0 0 0 / 30%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] .service-single:hover {
|
||||||
|
box-shadow: 0 0 15px rgb(85 85 85 / 30%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fontello-icon {
|
||||||
|
font-family: "fontello" !important;
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="service-area" id="board-area" style="padding-left: 0px;padding-right: 0px;">
|
||||||
|
<div class="container" style="width:100%;padding-left: 0px;padding-right: 0px;">
|
||||||
|
<div class="layui-carousel" id="board-switch" lay-filter="board-switch-filter"
|
||||||
|
style="background-color:rgba(0,0,0,0);padding-left: 0px;padding-right: 0px;">
|
||||||
|
<div id="mixly-board" carousel-item="" style="background-color:rgba(0,0,0,0);">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer id="footer" role="contentinfo" style="position: absolute; bottom: 0px;left: 0px;width: 100%;"></footer>
|
||||||
4
mixly/mixly-sw/templete/loader-div.html
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<div id="mixly-loader-div" style="display:none; margin:12px" align="center">
|
||||||
|
<progress id="mixly-loader"></progress>
|
||||||
|
<button id="mixly-loader-btn">{{d.btnName}}</button>
|
||||||
|
</div>
|
||||||
19
mixly/mixly-sw/templete/progress-bar-div.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<div class="layui-row layui-col-space15">
|
||||||
|
<div class="layui-col-md12">
|
||||||
|
<div class="layui-card layui-panel" id="{{d.boardPanelId}}">
|
||||||
|
<div class="layui-card-header">{{d.boardType}}</div>
|
||||||
|
<div class="layui-card-body">
|
||||||
|
<div style="padding: 0px;position: relative;height: 25px;">
|
||||||
|
<div class="layui-progress" lay-filter="{{d.progressFilter}}" lay-showPercent="yes"
|
||||||
|
style="position: absolute;left: 5px;right: 25px;top: 12px;display: inline-block;">
|
||||||
|
<div class="layui-progress-bar layui-bg-red" lay-percent="0%"></div>
|
||||||
|
</div>
|
||||||
|
<div style="display: inline-block;position: absolute;right: 5px;top: 2px;">
|
||||||
|
<i id="{{d.progressStatusId}}" class="progress-status layui-icon layui-icon-close-fill"
|
||||||
|
style="font-size: 15px; color: #1E9FFF;"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
405
mixly/mixly-sw/templete/setting-div.html
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
<style type="text/css">
|
||||||
|
#setting-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-menu-left {
|
||||||
|
width: 160px;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-menu-options {
|
||||||
|
margin: 5px;
|
||||||
|
width: 150px;
|
||||||
|
border-left-width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-menu-options > li > a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-menu-body {
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
left: 160px;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
/* border-radius: 0px 0px 5px 0px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] #setting-menu-body {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-menu-top {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-menu-bottom {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-menu-btn-group {
|
||||||
|
float: right;
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-menu .layui-table-header {
|
||||||
|
border-right-width: 5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layui-table-tips-c:before {
|
||||||
|
position: relative;
|
||||||
|
right: 1px;
|
||||||
|
top: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-menu-body .menu-body {
|
||||||
|
height: 100%;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-menu .layui-nav .layui-nav-item {
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-menu .layui-nav .layui-nav-item a {
|
||||||
|
padding-top: 1px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] #setting-menu .layui-bg-cyan {
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] #setting-menu .layui-nav .layui-nav-item.layui-this a {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #009688;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] #setting-menu .layui-nav .layui-nav-item a {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] #setting-menu .layui-nav-tree .layui-nav-item:hover {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
transition: 0.5s all;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] #setting-menu .layui-bg-cyan {
|
||||||
|
background-color: #2a2a2b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] #setting-menu .layui-nav .layui-this a {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--lay-color-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] #setting-menu .layui-nav .layui-nav-item a {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] #setting-menu .layui-nav-tree .layui-nav-item:hover {
|
||||||
|
background-color: #afa7a7;
|
||||||
|
transition: 0.5s all;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] #setting-menu .layui-table-tool {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] #setting-menu .layui-table-box {
|
||||||
|
border: 4px solid #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] #setting-menu .layui-table-box {
|
||||||
|
border: 4px solid #302d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-menu-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-menu-info > div:nth-child(1) {
|
||||||
|
height: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-menu-info > div:nth-child(2) {
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=light] .setting-menu-info > div:nth-child(2) {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-bs-theme=dark] .setting-menu-info > div:nth-child(2) {
|
||||||
|
border-top: 1px solid #454343;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-menu-user .layui-form-pane {
|
||||||
|
padding: 5px 5px 1px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-menu-update .layui-panel {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-menu-update .latest,
|
||||||
|
#setting-menu-update .obsolete {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-menu-update-server .mixly-progress {
|
||||||
|
display: none;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-menu-update-server .bar {
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-menu-update-server .bar > .progress {
|
||||||
|
/*display: none;*/
|
||||||
|
height: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 1px;
|
||||||
|
-webkit-box-shadow: unset;
|
||||||
|
box-shadow: unset;
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="setting-menu" class="layui-layer-wrap">
|
||||||
|
<div id="setting-menu-top">
|
||||||
|
<div id="setting-menu-left">
|
||||||
|
<ul id="setting-menu-options" class="layui-nav layui-nav-tree layui-bg-cyan layui-inline"
|
||||||
|
lay-filter="setting-menu-filter">
|
||||||
|
<li class="layui-nav-item layui-this" lay-id="0" data-type="config">
|
||||||
|
<a m-id="0">{{ d.personalise }}</a>
|
||||||
|
</li>
|
||||||
|
{{# if(d.env === 'electron'){ }}
|
||||||
|
<li class="layui-nav-item" lay-id="1" data-type="import-board">
|
||||||
|
<a m-id="1" href="javascript:;">{{ d.importBoard }}</a>
|
||||||
|
</li>
|
||||||
|
{{# } }}
|
||||||
|
{{# if(d.env === 'web-socket'){ }}
|
||||||
|
<li class="layui-nav-item" lay-id="2" data-type="ws-update">
|
||||||
|
<a m-id="2">{{ d.checkForUpdates }}</a>
|
||||||
|
</li>
|
||||||
|
{{# } }}
|
||||||
|
{{# if(d.env === 'nw'){ }}
|
||||||
|
<li class="layui-nav-item" lay-id="3" data-type="nw-update">
|
||||||
|
<a m-id="2">{{ d.checkForUpdates }}</a>
|
||||||
|
</li>
|
||||||
|
{{# } }}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div id="setting-menu-body">
|
||||||
|
<div class="menu-body layui-show">
|
||||||
|
<div id="setting-menu-user" class="setting-menu-info">
|
||||||
|
<div>
|
||||||
|
<div class="layui-collapse" lay-filter="menu-user-collapse-filter" lay-accordion="">
|
||||||
|
<div class="layui-colla-item">
|
||||||
|
<h2 class="layui-colla-title">{{ d.softwareSettings }}</h2>
|
||||||
|
<div class="layui-colla-content layui-show">
|
||||||
|
<form class="layui-form layui-form-pane" action="">
|
||||||
|
<div class="layui-form-item layui-form-text">
|
||||||
|
<label class="layui-form-label">{{ d.theme }}</label>
|
||||||
|
<div class="layui-input-block layui-row layui-col-space10">
|
||||||
|
<select class="setting-menu-item" value="theme" lay-ignore>
|
||||||
|
<option value="light">{{ d.light }}</option>
|
||||||
|
<option value="dark">{{ d.dark }}</option>
|
||||||
|
<option value="auto">{{ d.autoWithSys }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item layui-form-text">
|
||||||
|
<label class="layui-form-label">{{ d.language }}</label>
|
||||||
|
<div class="layui-input-block layui-row layui-col-space10">
|
||||||
|
<select class="setting-menu-item" value="language" lay-ignore>
|
||||||
|
<option value="zh-hans">简体中文</option>
|
||||||
|
<option value="zh-hant">繁體中文</option>
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="auto">{{ d.autoWithSys }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item layui-form-text">
|
||||||
|
<label class="layui-form-label">{{ d.cache }}</label>
|
||||||
|
<div class="layui-input-block layui-row layui-col-space10">
|
||||||
|
<select class="setting-menu-item" value="cache" lay-ignore>
|
||||||
|
<option value="yes">{{ d.yes }}</option>
|
||||||
|
<option value="no">{{ d.no }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{# if(d.env === 'electron'){ }}
|
||||||
|
<div class="layui-form-item layui-form-text">
|
||||||
|
<label class="layui-form-label">{{ d.autoUpdate }}</label>
|
||||||
|
<div class="layui-input-block layui-row layui-col-space10">
|
||||||
|
<select class="setting-menu-item" value="autoUpdate" lay-ignore>
|
||||||
|
<option value="yes">{{ d.yes }}</option>
|
||||||
|
<option value="no">{{ d.no }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{# } }}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{# if(d.env === 'electron'){ }}
|
||||||
|
<div class="layui-colla-item">
|
||||||
|
<h2 class="layui-colla-title">{{ d.boardSettings }}</h2>
|
||||||
|
<div class="layui-colla-content">
|
||||||
|
<form class="layui-form layui-form-pane" action="">
|
||||||
|
<div class="layui-form-item layui-form-text">
|
||||||
|
<label class="layui-form-label">{{ d.compileCAndH }}</label>
|
||||||
|
<div class="layui-input-block layui-row layui-col-space10">
|
||||||
|
<select class="setting-menu-item" value="compileCAndH" lay-ignore>
|
||||||
|
<option value="yes">{{ d.yes }}</option>
|
||||||
|
<option value="no">{{ d.no }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item layui-form-text">
|
||||||
|
<label class="layui-form-label">{{ d.autoOpenPort }}</label>
|
||||||
|
<div class="layui-input-block layui-row layui-col-space10">
|
||||||
|
<select class="setting-menu-item" value="autoOpenPort" lay-ignore>
|
||||||
|
<option value="yes">{{ d.yes }}</option>
|
||||||
|
<option value="no">{{ d.no }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{# } }}
|
||||||
|
<div class="layui-colla-item">
|
||||||
|
<h2 class="layui-colla-title">Blockly</h2>
|
||||||
|
<div class="layui-colla-content">
|
||||||
|
<form class="layui-form layui-form-pane" action="">
|
||||||
|
<div class="layui-form-item layui-form-text">
|
||||||
|
<label class="layui-form-label">{{ d.blocklyContentHighlight }}</label>
|
||||||
|
<div class="layui-input-block layui-row layui-col-space10">
|
||||||
|
<select class="setting-menu-item" value="blocklyContentHighlight"
|
||||||
|
lay-ignore>
|
||||||
|
<option value="no">{{ d.no }}</option>
|
||||||
|
<option value="yes">{{ d.yes }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item layui-form-text">
|
||||||
|
<label class="layui-form-label">{{ d.blocklyShowGrid }}</label>
|
||||||
|
<div class="layui-input-block layui-row layui-col-space10">
|
||||||
|
<select class="setting-menu-item" value="blocklyShowGrid" lay-ignore>
|
||||||
|
<option value="no">{{ d.no }}</option>
|
||||||
|
<option value="yes">{{ d.yes }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="layui-form-item layui-form-text">
|
||||||
|
<label class="layui-form-label">
|
||||||
|
{{ d.blocklyShowMinimap }} <a class="icon-beaker"
|
||||||
|
style="font-size: 14px;" title="{{ d.experimental }}"></a>
|
||||||
|
</label>
|
||||||
|
<div class="layui-input-block layui-row layui-col-space10">
|
||||||
|
<select class="setting-menu-item" value="blocklyShowMinimap" lay-ignore>
|
||||||
|
<option value="no">{{ d.no }}</option>
|
||||||
|
<option value="yes">{{ d.yes }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-item layui-form-text">
|
||||||
|
<label class="layui-form-label">
|
||||||
|
{{ d.blocklyMultiselect }} <a class="icon-beaker"
|
||||||
|
style="font-size: 14px;" title="{{ d.experimental }}"></a>
|
||||||
|
</label>
|
||||||
|
<div class="layui-input-block layui-row layui-col-space10">
|
||||||
|
<select class="setting-menu-item" value="blocklyMultiselect" lay-ignore>
|
||||||
|
<option value="no">{{ d.no }}</option>
|
||||||
|
<option value="yes">{{ d.yes }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
<div class="layui-form-item layui-form-text">
|
||||||
|
<label class="layui-form-label">{{ d.blockRenderer }}</label>
|
||||||
|
<div class="layui-input-block layui-row layui-col-space10">
|
||||||
|
<select class="setting-menu-item" value="blockRenderer" lay-ignore>
|
||||||
|
<option value="geras">geras</option>
|
||||||
|
<option value="thrasos">thrasos</option>
|
||||||
|
<option value="zelos">zelos</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="layui-btn self-adaption-btn layui-btn-sm" value="apply">{{ d.apply }}</button>
|
||||||
|
<button class="layui-btn self-adaption-btn layui-btn-sm" value="reset">{{ d.reset }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{# if(d.env === 'electron'){ }}
|
||||||
|
<div class="menu-body" id="import-board">
|
||||||
|
<div class="layui-hide" id="import-board-page" lay-filter="import-board-page-filter"></div>
|
||||||
|
</div>
|
||||||
|
{{# } }}
|
||||||
|
{{# if(d.env === 'web-socket' || d.env === 'nw'){ }}
|
||||||
|
<div class="menu-body">
|
||||||
|
<div id="setting-menu-update" class="setting-menu-info">
|
||||||
|
<div>
|
||||||
|
<div id="setting-menu-update-server" class="layui-card layui-panel">
|
||||||
|
<div class="layui-card-header">
|
||||||
|
<label>{{ d.server }}</label>
|
||||||
|
<span class="latest layui-badge layui-bg-green" value="latest">{{ d.latest }}</span>
|
||||||
|
<span class="obsolete layui-badge" value="obsolete">{{ d.obsolete }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="layui-card-body" class="">
|
||||||
|
<label>{{ d.version }}: </label>
|
||||||
|
<text></text>
|
||||||
|
<div class="mixly-progress ui teal progress">
|
||||||
|
<div class="bar">
|
||||||
|
<div class="progress"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="layui-btn self-adaption-btn layui-btn-sm" value="update">{{ d.update }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{# } }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="setting-menu-bottom">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
276
mixly/static-server/api.js
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const express = require('express');
|
||||||
|
const axios = require('axios');
|
||||||
|
const AdmZip = require('adm-zip');
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncAdmZip {
|
||||||
|
constructor(zipPath) {
|
||||||
|
this.zipPath = zipPath;
|
||||||
|
this.zip = new AdmZip(zipPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步解压到目录
|
||||||
|
async extractAllTo(targetPath, overwrite = true) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// 确保目标目录存在
|
||||||
|
fs.promises.mkdir(targetPath, { recursive: true })
|
||||||
|
.then(() => {
|
||||||
|
this.zip.extractAllTo(targetPath, overwrite);
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
targetPath,
|
||||||
|
fileCount: this.zip.getEntries().length
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步解压单个文件
|
||||||
|
async extractEntry(entryName, targetPath, overwrite = true) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const entry = this.zip.getEntry(entryName);
|
||||||
|
if (!entry) {
|
||||||
|
reject(new Error(`条目不存在: ${entryName}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.isDirectory) {
|
||||||
|
reject(new Error(`条目是目录: ${entryName}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保目标目录存在
|
||||||
|
const targetDir = path.dirname(targetPath);
|
||||||
|
fs.promises.mkdir(targetDir, { recursive: true })
|
||||||
|
.then(() => {
|
||||||
|
this.zip.extractEntryTo(entry, targetDir, false, overwrite);
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
entryName,
|
||||||
|
targetPath
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步获取zip文件信息
|
||||||
|
async getZipInfo() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const entries = this.zip.getEntries();
|
||||||
|
const info = {
|
||||||
|
fileCount: entries.length,
|
||||||
|
totalSize: entries.reduce((sum, entry) => sum + entry.header.size, 0),
|
||||||
|
entries: entries.map(entry => ({
|
||||||
|
name: entry.entryName,
|
||||||
|
size: entry.header.size,
|
||||||
|
isDirectory: entry.isDirectory,
|
||||||
|
compressedSize: entry.header.compressedSize
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
resolve(info);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
||||||
|
const TEMP_FOLDER_PATH = path.resolve(__dirname, '../temp');;
|
||||||
|
const VERSION_FILE = path.resolve(__dirname, '../version.json');
|
||||||
|
|
||||||
|
function getLocalVersion() {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(VERSION_FILE)) {
|
||||||
|
const data = fs.readFileSync(VERSION_FILE, 'utf8');
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('读取版本文件失败:', error);
|
||||||
|
}
|
||||||
|
return '2025.09.06';
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveVersionInfo(version) {
|
||||||
|
fs.writeFileSync(VERSION_FILE, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCloudVersion() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('http://update.mixly.cn/index.php');
|
||||||
|
return response.data.mixly.all;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取云端版本信息失败:', error);
|
||||||
|
return {
|
||||||
|
file: 'mixly.zip',
|
||||||
|
version: '2025.09.06'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkUpdate() {
|
||||||
|
try {
|
||||||
|
const localVersion = getLocalVersion();
|
||||||
|
const cloudVersions = await getCloudVersion();
|
||||||
|
const cloudVersion = cloudVersions['version'];
|
||||||
|
const cloudFile = 'http://update.mixly.cn/download.php?file=' + cloudVersions['file']
|
||||||
|
return {
|
||||||
|
needsUpdate: localVersion !== cloudVersion,
|
||||||
|
localVersion: localVersion,
|
||||||
|
cloudVersion: cloudVersion,
|
||||||
|
cloudFile: cloudFile,
|
||||||
|
error: ''
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
needsUpdate: false,
|
||||||
|
localVersion: '',
|
||||||
|
cloudVersion: '',
|
||||||
|
cloudFile: '',
|
||||||
|
error: error.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查更新接口
|
||||||
|
router.post('/check-update', async (req, res) => {
|
||||||
|
const updateInfo = await checkUpdate();
|
||||||
|
res.json(updateInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 下载进度返回
|
||||||
|
function deleteFolderRecursive(dirPath) {
|
||||||
|
if (fs.existsSync(dirPath)) {
|
||||||
|
fs.readdirSync(dirPath).forEach(file => {
|
||||||
|
const curPath = path.join(dirPath, file);
|
||||||
|
if (fs.lstatSync(curPath).isDirectory()) {
|
||||||
|
deleteFolderRecursive(curPath);
|
||||||
|
} else {
|
||||||
|
fs.unlinkSync(curPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fs.rmdirSync(dirPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get('/download', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { url, cloudVersion } = req.query;
|
||||||
|
|
||||||
|
// 清理临时文件夹
|
||||||
|
if (fs.existsSync(TEMP_FOLDER_PATH)) {
|
||||||
|
deleteFolderRecursive(TEMP_FOLDER_PATH);
|
||||||
|
}
|
||||||
|
fs.mkdirSync(TEMP_FOLDER_PATH, { recursive: true });
|
||||||
|
|
||||||
|
const filePath = path.resolve(TEMP_FOLDER_PATH, 'mixly.zip');
|
||||||
|
const fileStream = fs.createWriteStream(filePath);
|
||||||
|
|
||||||
|
// 设置 SSE 响应头
|
||||||
|
res.setHeader('Content-Type', 'text/event-stream');
|
||||||
|
res.setHeader('Cache-Control', 'no-cache');
|
||||||
|
res.setHeader('Connection', 'keep-alive');
|
||||||
|
res.flushHeaders();
|
||||||
|
|
||||||
|
// 发起下载请求 - 添加 NW.js 特定配置
|
||||||
|
const response = await axios({
|
||||||
|
method: 'GET',
|
||||||
|
url: url,
|
||||||
|
responseType: 'stream',
|
||||||
|
timeout: 60000,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||||
|
},
|
||||||
|
adapter: require('axios/lib/adapters/http')
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalSize = parseInt(response.headers['content-length'], 10) || 0;
|
||||||
|
|
||||||
|
let downloadedSize = 0;
|
||||||
|
let lastProgress = 0;
|
||||||
|
|
||||||
|
// 发送进度信息
|
||||||
|
const sendProgress = (progress) => {
|
||||||
|
if (progress !== lastProgress) {
|
||||||
|
const data = JSON.stringify({ type: 'progress', progress });
|
||||||
|
res.write(`data: ${data}\n\n`);
|
||||||
|
lastProgress = progress;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 管道流处理
|
||||||
|
response.data.pipe(fileStream);
|
||||||
|
|
||||||
|
// 进度监控
|
||||||
|
response.data.on('data', (chunk) => {
|
||||||
|
downloadedSize += chunk.length;
|
||||||
|
if (totalSize > 0) {
|
||||||
|
const progress = Math.round((downloadedSize / totalSize) * 100);
|
||||||
|
sendProgress(progress);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 文件流完成
|
||||||
|
fileStream.on('finish', async () => {
|
||||||
|
console.log('文件下载完成,开始解压');
|
||||||
|
|
||||||
|
// 发送解压信息
|
||||||
|
res.write(`data: ${JSON.stringify({ type: 'unzip' })}\n\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const asyncZip = new AsyncAdmZip(filePath);
|
||||||
|
await asyncZip.extractAllTo(path.resolve(__dirname, '../'));
|
||||||
|
|
||||||
|
// 保存版本信息
|
||||||
|
saveVersionInfo(cloudVersion);
|
||||||
|
|
||||||
|
// 发送完成信息
|
||||||
|
res.write(`data: ${JSON.stringify({ type: 'complete', version: cloudVersion })}\n\n`);
|
||||||
|
|
||||||
|
// 清理临时文件
|
||||||
|
if (fs.existsSync(TEMP_FOLDER_PATH)) {
|
||||||
|
deleteFolderRecursive(TEMP_FOLDER_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解压失败:', error);
|
||||||
|
res.write(`data: ${JSON.stringify({ type: 'error', message: '解压失败' })}\n\n`);
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
response.data.on('error', (error) => {
|
||||||
|
console.error('下载流错误:', error);
|
||||||
|
res.write(`data: ${JSON.stringify({ type: 'error', message: '下载流错误' })}\n\n`);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
fileStream.on('error', (error) => {
|
||||||
|
console.error('文件流错误:', error);
|
||||||
|
res.write(`data: ${JSON.stringify({ type: 'error', message: '文件保存错误' })}\n\n`);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载过程错误:', error);
|
||||||
|
res.write(`data: ${JSON.stringify({ type: 'error', message: '下载失败: ' + error.message })}\n\n`);
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
20
mixly/static-server/certs/server.crt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDNTCCAh0CFGQSgGkvHiuKgOSMVVjT1dFX1Df3MA0GCSqGSIb3DQEBCwUAMFcx
|
||||||
|
CzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAwDgYDVQQHDAdCZWlqaW5n
|
||||||
|
MQ4wDAYDVQQKDAVNaXhseTEUMBIGA1UEAwwLMTkyLjE2OC4xLjEwHhcNMjIwODE5
|
||||||
|
MDg1ODA4WhcNMjMwODE5MDg1ODA4WjBXMQswCQYDVQQGEwJDTjEQMA4GA1UECAwH
|
||||||
|
QmVpamluZzEQMA4GA1UEBwwHQmVpamluZzEOMAwGA1UECgwFTWl4bHkxFDASBgNV
|
||||||
|
BAMMCzE5Mi4xNjguMS4xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
|
||||||
|
yxvOxepem9zCBTGZ2aMjba73xkVgljGuEhbPMgRS8l763ril2rapfGoiBVEGOkTv
|
||||||
|
RY6YFP6yNtX4E8SXuePcvS75g+XUVgUYLmXJ+2DZfNL/RLnTRciiGR/n53BEbE9R
|
||||||
|
c81I/yYbCmWJH63F0Zn4SOghzP9AyBSTkVVHt2onm4NNCWEhsmAFlHOsiNQeOd4r
|
||||||
|
Ji5k9KFjhstKAtlDr/HLjTTew15y2GVhusjko52HYM9zYrNOPrOHzuQza4cxln37
|
||||||
|
FQUZudJx1TttWJ4u8z8ycAHNJ+Nj9yUzIo55xe/R+/qpF+ADxVMbm+Dpx2wIp6iG
|
||||||
|
9WZSBWXu+6tfTuUjDsDUNwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQC4gqiSyqpb
|
||||||
|
8Z8p+VqF3ezJXMBtTyOnBsWXAO8UUJ8pEIrTVWBj+MQfbrYwX54omApNra8CGC+g
|
||||||
|
yGa84QNVRgor71sV+rpwXu/+2vc1eExJTjsias4TSgmzJYGH2NLcNxAoru7KQjzv
|
||||||
|
Inrn88QMcDdhIkKTjxpu1uz75p62oMD6Iv58n+4iBLxfW8GPQ5U8BKOVps+DWTOI
|
||||||
|
lDMtuc4ZySLDUNn8tHsKSLL41kupmkqAd9G12bqR+BOs06Nf/IeVuYGtZDw5gZxg
|
||||||
|
DpKb826ZTsI7ED34qhG+4mol6py8wM3UeDJ8QX9EANwwDA7DJ1boC8QIxAk5fgA1
|
||||||
|
rzyUgc30MMx0
|
||||||
|
-----END CERTIFICATE-----
|
||||||
16
mixly/static-server/certs/server.csr
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
-----BEGIN CERTIFICATE REQUEST-----
|
||||||
|
MIICnDCCAYQCAQAwVzELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAO
|
||||||
|
BgNVBAcMB0JlaWppbmcxDjAMBgNVBAoMBU1peGx5MRQwEgYDVQQDDAsxOTIuMTY4
|
||||||
|
LjEuMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMsbzsXqXpvcwgUx
|
||||||
|
mdmjI22u98ZFYJYxrhIWzzIEUvJe+t64pdq2qXxqIgVRBjpE70WOmBT+sjbV+BPE
|
||||||
|
l7nj3L0u+YPl1FYFGC5lyftg2XzS/0S500XIohkf5+dwRGxPUXPNSP8mGwpliR+t
|
||||||
|
xdGZ+EjoIcz/QMgUk5FVR7dqJ5uDTQlhIbJgBZRzrIjUHjneKyYuZPShY4bLSgLZ
|
||||||
|
Q6/xy4003sNecthlYbrI5KOdh2DPc2KzTj6zh87kM2uHMZZ9+xUFGbnScdU7bVie
|
||||||
|
LvM/MnABzSfjY/clMyKOecXv0fv6qRfgA8VTG5vg6cdsCKeohvVmUgVl7vurX07l
|
||||||
|
Iw7A1DcCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQBiNeSZNC0CFlxd1lxhu0cb
|
||||||
|
+w5T2ikskgKUyxe/5ZDVFfzXaDw/JrE/9sdsnecl/t0Wbsir5lRLH0LOe0XyusuD
|
||||||
|
DcVeOEVUjVWTSgtR5Te9pjKHBphyu0fdHQG6LKNa6UGThc6aaKyjkxRv8dFIKkjl
|
||||||
|
FHceesd51pBKRdIhmFXg9kx+9iJVKgLL92U8uKgsjZBB4ZzYaUD9RLaPxaK52gOf
|
||||||
|
y6TswblJbCUxEl8vCIyB1v5rGngJ7kCjuEk7lNDSRI58GBkEzFQYLHAi7C/BOLDY
|
||||||
|
DDpJaPr9crfmD3UboAqgLdi5S8W2BN59VcLUtp4/2bgfcvYQFUahKvyTymf6OmU9
|
||||||
|
-----END CERTIFICATE REQUEST-----
|
||||||
27
mixly/static-server/certs/server.key
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEAyxvOxepem9zCBTGZ2aMjba73xkVgljGuEhbPMgRS8l763ril
|
||||||
|
2rapfGoiBVEGOkTvRY6YFP6yNtX4E8SXuePcvS75g+XUVgUYLmXJ+2DZfNL/RLnT
|
||||||
|
RciiGR/n53BEbE9Rc81I/yYbCmWJH63F0Zn4SOghzP9AyBSTkVVHt2onm4NNCWEh
|
||||||
|
smAFlHOsiNQeOd4rJi5k9KFjhstKAtlDr/HLjTTew15y2GVhusjko52HYM9zYrNO
|
||||||
|
PrOHzuQza4cxln37FQUZudJx1TttWJ4u8z8ycAHNJ+Nj9yUzIo55xe/R+/qpF+AD
|
||||||
|
xVMbm+Dpx2wIp6iG9WZSBWXu+6tfTuUjDsDUNwIDAQABAoIBABOlPevQzpPe13lv
|
||||||
|
IcV2TR/304l+/meoqIChaisZVfiRjUxrqccs8dnR3jaLbsHGFyqwLy+grxY0vgkT
|
||||||
|
c+WMD7bQy1uhqFclqQAb4lyJMqArPHumSbQvQtaRSnoNVuDvDx7XVV8wjV8FES1a
|
||||||
|
Po8WiHhs05AjhF2V9+wPxp8MCoa1EWMh+A9gkswvbaDrh+YTC3qori2H9gvt4DPp
|
||||||
|
feb1ofiMLSM2HzUtJgw+kp5SvWtYvg001f8pO7A2KIV0MoE38tXuaebxta0YHql+
|
||||||
|
BuICWCe2M7RZau2JHZM51tPQ2/3l6ya2YSwdPPRCUQqpJ8uFS1M1QnjZKFlAEXY/
|
||||||
|
KU4lmskCgYEA7J9EyIMqfI6pbGLeLnbf8aN/6C5ldOvEYZThlcjDpOMveajmylYs
|
||||||
|
P0SFsK05MaFf5PeM0U2WHfzx8bLyLUvIpICvJ5w9q9vR/ZOxHCBd87dMqjVDFgXm
|
||||||
|
iUz+m87D7wVNFKzZF+lnqdqOpouuBfMlJqbCygmcw4ISEwntOnDRsy0CgYEA273v
|
||||||
|
eRMTXM+VmJ4ZcTYXpRFGOFqP8yITyQ8dm7Yr+RpJAxLS/fN9DZEsl8xWNevFmbp4
|
||||||
|
isDtKNKjmQIsvpxt2s30dpqg53YWnfHjFVN/JLYbxzYavYcciJ/Apl6RyRYukTPC
|
||||||
|
T99gUMfu+/7LHgS2ytzZ0kmVBmKaXAwUzU3lE3MCgYAYm/frYrjoe23jd+TjsDla
|
||||||
|
SEblPu4OWvbxrypHCbpPS9GENazLHms7qUS+O0XXg5EVnylmG0uhks0W9iV50Ift
|
||||||
|
k/SjifxgA1yzosioxDUBQ+8VRLTVdYekf/169uYp1cNOgyuQ8RV29OQhLiXLOJ6E
|
||||||
|
hpN7r8Q+ESkQEdg6W8FzgQKBgHKbZHvsVAvzBJ39z105jil8kfgwW6W+Xz1dEd81
|
||||||
|
q0eXyv68Yakrxkw+LFjbrRcgagYcuGP97XN+MO9LsBSWN8GH63m0ejleYLtt/jcQ
|
||||||
|
Pl7iUCidcmLpRhuH3o2nAzgyxoTazvyjj3NyY5WwtTVp1gCGIWFJGV2kLcfWUT8m
|
||||||
|
4lQ7AoGBAM5R5jI9n8ndNhGpoIisu8YCeowS5QGDJznBN932NwenUgiZw4suv71r
|
||||||
|
v5FAdjIMKwSQcjaZoPPqqTuXnTIo1mXQpiMitAAXeb1HmJnPvZgSk5RI3OnrOZDE
|
||||||
|
CClJzjTTEprhaw9LR7uqWUp12f1Z+lAq9kQFAeYBarpqIplH6/Gi
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
31
mixly/static-server/server.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const StaticServer = require('./static-server.js');
|
||||||
|
const SSLStaticServer = require('./static-sslserver.js');
|
||||||
|
|
||||||
|
|
||||||
|
function deleteFile(filePath) {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stats = fs.statSync(filePath);
|
||||||
|
if (stats.isFile()) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
console.log('File deleted successfully.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deleting file:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
StaticServer.run('7000');
|
||||||
|
SSLStaticServer.run('8000');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!module.parent) {
|
||||||
|
deleteFile('./nw_cache/Default/Preferences');
|
||||||
|
init();
|
||||||
|
} else {
|
||||||
|
module.exports = init;
|
||||||
|
}
|
||||||
22
mixly/static-server/static-server.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const http = require('http');
|
||||||
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
|
const apiRoutes = require('./api.js');
|
||||||
|
|
||||||
|
const StaticServer = {};
|
||||||
|
|
||||||
|
StaticServer.run = (port) => {
|
||||||
|
const app = express();
|
||||||
|
app.use(express.static(path.resolve(__dirname, '../')));
|
||||||
|
app.use('/api/', apiRoutes);
|
||||||
|
const httpServer = http.createServer(app);
|
||||||
|
httpServer.listen(port);
|
||||||
|
console.log('Static服务器正在运行 [端口 - ' + port + ', http]...');
|
||||||
|
console.log('访问地址:http://127.0.0.1:' + port);
|
||||||
|
StaticServer.server = httpServer;
|
||||||
|
StaticServer.app = app;
|
||||||
|
StaticServer.port = port;
|
||||||
|
StaticServer.protocol = 'http';
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = StaticServer;
|
||||||
27
mixly/static-server/static-sslserver.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const https = require('https');
|
||||||
|
const express = require('express');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const SSLStaticServer = {};
|
||||||
|
|
||||||
|
SSLStaticServer.run = (port) => {
|
||||||
|
const KEY_PATH = path.resolve(__dirname, './certs/server.key');
|
||||||
|
const CRT_PATH = path.resolve(__dirname, './certs/server.crt');
|
||||||
|
const options = {
|
||||||
|
key: fs.readFileSync(KEY_PATH),
|
||||||
|
cert: fs.readFileSync(CRT_PATH)
|
||||||
|
};
|
||||||
|
const app = express();
|
||||||
|
app.use(express.static(path.resolve(__dirname, '../')));
|
||||||
|
const httpsServer = https.createServer(options, app);
|
||||||
|
httpsServer.listen(port);
|
||||||
|
console.log('Static服务器正在运行 [端口 - ' + port + ', https]...');
|
||||||
|
console.log('访问地址:https://127.0.0.1:' + port);
|
||||||
|
SSLStaticServer.server = httpsServer;
|
||||||
|
SSLStaticServer.app = app;
|
||||||
|
SSLStaticServer.port = port;
|
||||||
|
SSLStaticServer.protocol = 'https';
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SSLStaticServer;
|
||||||
0
mixly/tools/python/ampy/__init__.py
Normal file
552
mixly/tools/python/ampy/cli.py
Normal file
@@ -0,0 +1,552 @@
|
|||||||
|
# Adafruit MicroPython Tool - Command Line Interface
|
||||||
|
# Author: Tony DiCola
|
||||||
|
# Copyright (c) 2016 Adafruit Industries
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
from __future__ import print_function
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import posixpath
|
||||||
|
import re
|
||||||
|
import serial.serialutil
|
||||||
|
import binascii
|
||||||
|
import click
|
||||||
|
import dotenv
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Load AMPY_PORT et al from .ampy file
|
||||||
|
# Performed here because we need to beat click's decorators.
|
||||||
|
config = dotenv.find_dotenv(filename=".ampy", usecwd=True)
|
||||||
|
if config:
|
||||||
|
dotenv.load_dotenv(dotenv_path=config)
|
||||||
|
|
||||||
|
import ampy.files as files
|
||||||
|
import ampy.pyboard as pyboard
|
||||||
|
|
||||||
|
|
||||||
|
_board = None
|
||||||
|
|
||||||
|
|
||||||
|
def windows_full_port_name(portname):
|
||||||
|
# Helper function to generate proper Windows COM port paths. Apparently
|
||||||
|
# Windows requires COM ports above 9 to have a special path, where ports below
|
||||||
|
# 9 are just referred to by COM1, COM2, etc. (wacky!) See this post for
|
||||||
|
# more info and where this code came from:
|
||||||
|
# http://eli.thegreenplace.net/2009/07/31/listing-all-serial-ports-on-windows-with-python/
|
||||||
|
m = re.match("^COM(\d+)$", portname)
|
||||||
|
if m and int(m.group(1)) < 10:
|
||||||
|
return portname
|
||||||
|
else:
|
||||||
|
return "\\\\.\\{0}".format(portname)
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.option(
|
||||||
|
"--port",
|
||||||
|
"-p",
|
||||||
|
envvar="AMPY_PORT",
|
||||||
|
required=True,
|
||||||
|
type=click.STRING,
|
||||||
|
help="Name of serial port for connected board. Can optionally specify with AMPY_PORT environment variable.",
|
||||||
|
metavar="PORT",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--baud",
|
||||||
|
"-b",
|
||||||
|
envvar="AMPY_BAUD",
|
||||||
|
default=115200,
|
||||||
|
type=click.INT,
|
||||||
|
help="Baud rate for the serial connection (default 115200). Can optionally specify with AMPY_BAUD environment variable.",
|
||||||
|
metavar="BAUD",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--delay",
|
||||||
|
"-d",
|
||||||
|
envvar="AMPY_DELAY",
|
||||||
|
default=0,
|
||||||
|
type=click.FLOAT,
|
||||||
|
help="Delay in seconds before entering RAW MODE (default 0). Can optionally specify with AMPY_DELAY environment variable.",
|
||||||
|
metavar="DELAY",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--reset",
|
||||||
|
"-r",
|
||||||
|
envvar="AMPY_RESET",
|
||||||
|
default="{}",
|
||||||
|
type=click.STRING,
|
||||||
|
help="default={}",
|
||||||
|
metavar="RESET",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--empty",
|
||||||
|
"-e",
|
||||||
|
envvar="AMPY_EMPTY",
|
||||||
|
default="main.py",
|
||||||
|
type=click.STRING,
|
||||||
|
help="default=main.py",
|
||||||
|
metavar="EMPTY",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--info",
|
||||||
|
"-i",
|
||||||
|
envvar="AMPY_INFO",
|
||||||
|
default=True,
|
||||||
|
type=click.BOOL,
|
||||||
|
help="default=True",
|
||||||
|
metavar="INFO",
|
||||||
|
)
|
||||||
|
@click.version_option()
|
||||||
|
def cli(port, baud, delay, reset="{}", empty="main.py", info=True):
|
||||||
|
"""ampy - Adafruit MicroPython Tool
|
||||||
|
|
||||||
|
Ampy is a tool to control MicroPython boards over a serial connection. Using
|
||||||
|
ampy you can manipulate files on the board's internal filesystem and even run
|
||||||
|
scripts.
|
||||||
|
"""
|
||||||
|
global _board
|
||||||
|
# On Windows fix the COM port path name for ports above 9 (see comment in
|
||||||
|
# windows_full_port_name function).
|
||||||
|
#sys.stdout.write(reset + "\n")
|
||||||
|
#sys.stdout.flush()
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
port = windows_full_port_name(port)
|
||||||
|
_board = pyboard.Pyboard(port, baudrate=baud, rawdelay=delay, boardreset=reset, file_empty=empty, info=info)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument("remote_file")
|
||||||
|
@click.argument("local_file", type=click.File("wb"), required=False)
|
||||||
|
def get(remote_file, local_file):
|
||||||
|
"""
|
||||||
|
Retrieve a file from the board.
|
||||||
|
|
||||||
|
Get will download a file from the board and print its contents or save it
|
||||||
|
locally. You must pass at least one argument which is the path to the file
|
||||||
|
to download from the board. If you don't specify a second argument then
|
||||||
|
the file contents will be printed to standard output. However if you pass
|
||||||
|
a file name as the second argument then the contents of the downloaded file
|
||||||
|
will be saved to that file (overwriting anything inside it!).
|
||||||
|
|
||||||
|
For example to retrieve the boot.py and print it out run:
|
||||||
|
|
||||||
|
ampy --port /board/serial/port get boot.py
|
||||||
|
|
||||||
|
Or to get main.py and save it as main.py locally run:
|
||||||
|
|
||||||
|
ampy --port /board/serial/port get main.py main.py
|
||||||
|
"""
|
||||||
|
# Get the file contents.
|
||||||
|
board_files = files.Files(_board)
|
||||||
|
contents = board_files.get(remote_file)
|
||||||
|
# Print the file out if no local file was provided, otherwise save it.
|
||||||
|
if local_file is None:
|
||||||
|
contents = str(contents)[2:-1]
|
||||||
|
print(contents, end='')
|
||||||
|
else:
|
||||||
|
value = binascii.unhexlify(contents)
|
||||||
|
local_file.write(value.decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option(
|
||||||
|
"--exists-okay", is_flag=True, help="Ignore if the directory already exists."
|
||||||
|
)
|
||||||
|
@click.argument("directory")
|
||||||
|
def mkdir(directory, exists_okay):
|
||||||
|
"""
|
||||||
|
Create a directory on the board.
|
||||||
|
|
||||||
|
Mkdir will create the specified directory on the board. One argument is
|
||||||
|
required, the full path of the directory to create.
|
||||||
|
|
||||||
|
Note that you cannot recursively create a hierarchy of directories with one
|
||||||
|
mkdir command, instead you must create each parent directory with separate
|
||||||
|
mkdir command calls.
|
||||||
|
|
||||||
|
For example to make a directory under the root called 'code':
|
||||||
|
|
||||||
|
ampy --port /board/serial/port mkdir /code
|
||||||
|
"""
|
||||||
|
# Run the mkdir command.
|
||||||
|
board_files = files.Files(_board)
|
||||||
|
board_files.mkdir(directory, exists_okay=exists_okay)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument("file")
|
||||||
|
def mkfile(file):
|
||||||
|
board_files = files.Files(_board)
|
||||||
|
board_files.mkfile(file)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument("directory", default="/")
|
||||||
|
@click.option(
|
||||||
|
"--long_format",
|
||||||
|
"-l",
|
||||||
|
is_flag=True,
|
||||||
|
help="Print long format info including size of files. Note the size of directories is not supported and will show 0 values.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--recursive",
|
||||||
|
"-r",
|
||||||
|
is_flag=True,
|
||||||
|
help="recursively list all files and (empty) directories.",
|
||||||
|
)
|
||||||
|
def ls(directory, long_format, recursive):
|
||||||
|
"""List contents of a directory on the board.
|
||||||
|
|
||||||
|
Can pass an optional argument which is the path to the directory. The
|
||||||
|
default is to list the contents of the root, /, path.
|
||||||
|
|
||||||
|
For example to list the contents of the root run:
|
||||||
|
|
||||||
|
ampy --port /board/serial/port ls
|
||||||
|
|
||||||
|
Or to list the contents of the /foo/bar directory on the board run:
|
||||||
|
|
||||||
|
ampy --port /board/serial/port ls /foo/bar
|
||||||
|
|
||||||
|
Add the -l or --long_format flag to print the size of files (however note
|
||||||
|
MicroPython does not calculate the size of folders and will show 0 bytes):
|
||||||
|
|
||||||
|
ampy --port /board/serial/port ls -l /foo/bar
|
||||||
|
"""
|
||||||
|
# List each file/directory on a separate line.
|
||||||
|
board_files = files.Files(_board)
|
||||||
|
for f in board_files.ls(directory, long_format=long_format, recursive=recursive):
|
||||||
|
print(f)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument("local", type=click.Path(exists=True))
|
||||||
|
@click.argument("remote", required=False)
|
||||||
|
def put(local, remote):
|
||||||
|
"""Put a file or folder and its contents on the board.
|
||||||
|
|
||||||
|
Put will upload a local file or folder to the board. If the file already
|
||||||
|
exists on the board it will be overwritten with no warning! You must pass
|
||||||
|
at least one argument which is the path to the local file/folder to
|
||||||
|
upload. If the item to upload is a folder then it will be copied to the
|
||||||
|
board recursively with its entire child structure. You can pass a second
|
||||||
|
optional argument which is the path and name of the file/folder to put to
|
||||||
|
on the connected board.
|
||||||
|
|
||||||
|
For example to upload a main.py from the current directory to the board's
|
||||||
|
root run:
|
||||||
|
|
||||||
|
ampy --port /board/serial/port put main.py
|
||||||
|
|
||||||
|
Or to upload a board_boot.py from a ./foo subdirectory and save it as boot.py
|
||||||
|
in the board's root run:
|
||||||
|
|
||||||
|
ampy --port /board/serial/port put ./foo/board_boot.py boot.py
|
||||||
|
|
||||||
|
To upload a local folder adafruit_library and all of its child files/folders
|
||||||
|
as an item under the board's root run:
|
||||||
|
|
||||||
|
ampy --port /board/serial/port put adafruit_library
|
||||||
|
|
||||||
|
Or to put a local folder adafruit_library on the board under the path
|
||||||
|
/lib/adafruit_library on the board run:
|
||||||
|
|
||||||
|
ampy --port /board/serial/port put adafruit_library /lib/adafruit_library
|
||||||
|
"""
|
||||||
|
# Use the local filename if no remote filename is provided.
|
||||||
|
|
||||||
|
# Check if path is a folder and do recursive copy of everything inside it.
|
||||||
|
# Otherwise it's a file and should simply be copied over.
|
||||||
|
|
||||||
|
if os.path.isdir(local):
|
||||||
|
if remote is None:
|
||||||
|
remote = ""
|
||||||
|
# Directory copy, create the directory and walk all children to copy
|
||||||
|
# over the files.
|
||||||
|
#print("true")
|
||||||
|
#print(remote)
|
||||||
|
board_files = files.Files(_board)
|
||||||
|
board_files._pyboard.enter_raw_repl()
|
||||||
|
file_empty = board_files._pyboard.file_empty
|
||||||
|
# sys.stdout.write("Empty ./{}...\n".format(file_empty))
|
||||||
|
# sys.stdout.flush()
|
||||||
|
# board_files.put('./{}'.format(file_empty), '', False, False)
|
||||||
|
# sys.stdout.write("Empty ./{} Done!\n".format(file_empty))
|
||||||
|
# sys.stdout.flush()
|
||||||
|
files_info = board_files.getFilesInfo('')
|
||||||
|
files_dict = {}
|
||||||
|
files_info_len = len(files_info)
|
||||||
|
for i in range(0, files_info_len, 1):
|
||||||
|
if files_info[i][0][0] == '/':
|
||||||
|
files_info[i][0] = files_info[i][0][1 : len(files_info[i][0])]
|
||||||
|
files_dict[files_info[i][0]] = files_info[i][1]
|
||||||
|
# sys.stdout.write(str(files_dict))
|
||||||
|
# sys.stdout.flush()
|
||||||
|
for parent, child_dirs, child_files in os.walk(local, followlinks=True):
|
||||||
|
# Create board filesystem absolute path to parent directory.
|
||||||
|
|
||||||
|
remote_parent = posixpath.normpath(
|
||||||
|
posixpath.join(remote, os.path.relpath(parent, local))
|
||||||
|
)
|
||||||
|
#print(remote_parent)
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
# Create remote parent directory.
|
||||||
|
print(remote_parent)
|
||||||
|
board_files.mkdir(remote_parent)
|
||||||
|
except files.DirectoryExistsError:
|
||||||
|
# Ignore errors for directories that already exist.
|
||||||
|
pass
|
||||||
|
# Loop through all the files and put them on the board too.
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
file_name_list = []
|
||||||
|
data_list = []
|
||||||
|
for filename in child_files:
|
||||||
|
file_path = os.path.join(parent, filename)
|
||||||
|
if remote_parent == '.':
|
||||||
|
remote_filename = filename
|
||||||
|
else:
|
||||||
|
remote_filename = posixpath.join(remote_parent, filename)
|
||||||
|
file_size = os.path.getsize(file_path)
|
||||||
|
board_file_size = files_dict.get(remote_filename, -1)
|
||||||
|
# sys.stdout.write('name {0} size {1} {2}\n'.format(remote_filename, board_file_size, file_size))
|
||||||
|
# sys.stdout.flush()
|
||||||
|
if board_file_size != file_size:
|
||||||
|
with open(file_path, "rb") as infile:
|
||||||
|
file_name_list.append(remote_filename)
|
||||||
|
data_list.append(infile.read())
|
||||||
|
#board_files.put(remote_filename, infile.read())
|
||||||
|
else:
|
||||||
|
sys.stdout.write("Skip " + filename + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
board_files.putdir(file_name_list, data_list, False)
|
||||||
|
|
||||||
|
'''
|
||||||
|
for filename in child_files:
|
||||||
|
with open(os.path.join(parent, filename), "rb") as infile:
|
||||||
|
remote_filename = posixpath.join(remote_parent, filename)
|
||||||
|
board_files.put(remote_filename, infile.read())
|
||||||
|
'''
|
||||||
|
|
||||||
|
else:
|
||||||
|
if remote is None:
|
||||||
|
remote = os.path.basename(os.path.abspath(local))
|
||||||
|
# File copy, open the file and copy its contents to the board.
|
||||||
|
# Put the file on the board.
|
||||||
|
with open(local, "rb") as infile:
|
||||||
|
board_files = files.Files(_board)
|
||||||
|
board_files.put(remote, infile.read())
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument("remote_file")
|
||||||
|
def rm(remote_file):
|
||||||
|
"""Remove a file from the board.
|
||||||
|
|
||||||
|
Remove the specified file from the board's filesystem. Must specify one
|
||||||
|
argument which is the path to the file to delete. Note that this can't
|
||||||
|
delete directories which have files inside them, but can delete empty
|
||||||
|
directories.
|
||||||
|
|
||||||
|
For example to delete main.py from the root of a board run:
|
||||||
|
|
||||||
|
ampy --port /board/serial/port rm main.py
|
||||||
|
"""
|
||||||
|
# Delete the provided file/directory on the board.
|
||||||
|
board_files = files.Files(_board)
|
||||||
|
board_files.rm(remote_file)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option(
|
||||||
|
"--missing-okay", is_flag=True, help="Ignore if the directory does not exist."
|
||||||
|
)
|
||||||
|
@click.argument("remote_folder")
|
||||||
|
def rmdir(remote_folder, missing_okay):
|
||||||
|
"""Forcefully remove a folder and all its children from the board.
|
||||||
|
|
||||||
|
Remove the specified folder from the board's filesystem. Must specify one
|
||||||
|
argument which is the path to the folder to delete. This will delete the
|
||||||
|
directory and ALL of its children recursively, use with caution!
|
||||||
|
|
||||||
|
For example to delete everything under /adafruit_library from the root of a
|
||||||
|
board run:
|
||||||
|
|
||||||
|
ampy --port /board/serial/port rmdir adafruit_library
|
||||||
|
"""
|
||||||
|
# Delete the provided file/directory on the board.
|
||||||
|
board_files = files.Files(_board)
|
||||||
|
board_files.rmdir(remote_folder, missing_okay=missing_okay)
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument("oldname")
|
||||||
|
@click.argument("newname")
|
||||||
|
def rename(oldname, newname):
|
||||||
|
board_files = files.Files(_board)
|
||||||
|
board_files.rename(oldname, newname)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument("local", required=True)
|
||||||
|
@click.argument("remote", required=True)
|
||||||
|
def cpdir(local, remote):
|
||||||
|
board_files = files.Files(_board)
|
||||||
|
board_files.cpdir(local, remote)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument("local", required=True)
|
||||||
|
@click.argument("remote", required=True)
|
||||||
|
def cpfile(local, remote):
|
||||||
|
board_files = files.Files(_board)
|
||||||
|
board_files.cpfile(local, remote)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument("local_file")
|
||||||
|
@click.option(
|
||||||
|
"--no-output",
|
||||||
|
"-n",
|
||||||
|
is_flag=True,
|
||||||
|
help="Run the code without waiting for it to finish and print output. Use this when running code with main loops that never return.",
|
||||||
|
)
|
||||||
|
def run(local_file, no_output):
|
||||||
|
"""Run a script and print its output.
|
||||||
|
|
||||||
|
Run will send the specified file to the board and execute it immediately.
|
||||||
|
Any output from the board will be printed to the console (note that this is
|
||||||
|
not a 'shell' and you can't send input to the program).
|
||||||
|
|
||||||
|
Note that if your code has a main or infinite loop you should add the --no-output
|
||||||
|
option. This will run the script and immediately exit without waiting for
|
||||||
|
the script to finish and print output.
|
||||||
|
|
||||||
|
For example to run a test.py script and print any output until it finishes:
|
||||||
|
|
||||||
|
ampy --port /board/serial/port run test.py
|
||||||
|
|
||||||
|
Or to run test.py and not wait for it to finish:
|
||||||
|
|
||||||
|
ampy --port /board/serial/port run --no-output test.py
|
||||||
|
"""
|
||||||
|
# Run the provided file and print its output.
|
||||||
|
board_files = files.Files(_board)
|
||||||
|
try:
|
||||||
|
output = board_files.run(local_file, not no_output, not no_output)
|
||||||
|
if output is not None:
|
||||||
|
print(output.decode("utf-8"), end="")
|
||||||
|
except IOError:
|
||||||
|
click.echo(
|
||||||
|
"Failed to find or read input file: {0}".format(local_file), err=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option(
|
||||||
|
"--bootloader", "mode", flag_value="BOOTLOADER", help="Reboot into the bootloader"
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--hard",
|
||||||
|
"mode",
|
||||||
|
flag_value="NORMAL",
|
||||||
|
help="Perform a hard reboot, including running init.py",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--repl",
|
||||||
|
"mode",
|
||||||
|
flag_value="SOFT",
|
||||||
|
default=True,
|
||||||
|
help="Perform a soft reboot, entering the REPL [default]",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--safe",
|
||||||
|
"mode",
|
||||||
|
flag_value="SAFE_MODE",
|
||||||
|
help="Perform a safe-mode reboot. User code will not be run and the filesystem will be writeable over USB",
|
||||||
|
)
|
||||||
|
def reset(mode):
|
||||||
|
"""Perform soft reset/reboot of the board.
|
||||||
|
|
||||||
|
Will connect to the board and perform a reset. Depending on the board
|
||||||
|
and firmware, several different types of reset may be supported.
|
||||||
|
|
||||||
|
ampy --port /board/serial/port reset
|
||||||
|
"""
|
||||||
|
_board.enter_raw_repl()
|
||||||
|
if mode == "SOFT":
|
||||||
|
_board.exit_raw_repl()
|
||||||
|
return
|
||||||
|
|
||||||
|
_board.exec_(
|
||||||
|
"""if 1:
|
||||||
|
def on_next_reset(x):
|
||||||
|
try:
|
||||||
|
import microcontroller
|
||||||
|
except:
|
||||||
|
if x == 'NORMAL': return ''
|
||||||
|
return 'Reset mode only supported on CircuitPython'
|
||||||
|
try:
|
||||||
|
microcontroller.on_next_reset(getattr(microcontroller.RunMode, x))
|
||||||
|
except ValueError as e:
|
||||||
|
return str(e)
|
||||||
|
return ''
|
||||||
|
def reset():
|
||||||
|
try:
|
||||||
|
import microcontroller
|
||||||
|
except:
|
||||||
|
import machine as microcontroller
|
||||||
|
microcontroller.reset()
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
r = _board.eval("on_next_reset({})".format(repr(mode)))
|
||||||
|
print("here we are", repr(r))
|
||||||
|
if r:
|
||||||
|
click.echo(r, err=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
_board.exec_raw_no_follow("reset()")
|
||||||
|
except serial.serialutil.SerialException as e:
|
||||||
|
# An error is expected to occur, as the board should disconnect from
|
||||||
|
# serial when restarted via microcontroller.reset()
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
error_exit = False
|
||||||
|
try:
|
||||||
|
cli()
|
||||||
|
except BaseException as e:
|
||||||
|
if getattr(e, 'code', True):
|
||||||
|
print('Error: {}'.format(e))
|
||||||
|
error_exit = True
|
||||||
|
finally:
|
||||||
|
# Try to ensure the board serial connection is always gracefully closed.
|
||||||
|
if _board is not None:
|
||||||
|
try:
|
||||||
|
_board.close()
|
||||||
|
except:
|
||||||
|
# Swallow errors when attempting to close as it's just a best effort
|
||||||
|
# and shouldn't cause a new error or problem if the connection can't
|
||||||
|
# be closed.
|
||||||
|
pass
|
||||||
|
if error_exit:
|
||||||
|
sys.exit(1)
|
||||||
594
mixly/tools/python/ampy/files.py
Normal file
@@ -0,0 +1,594 @@
|
|||||||
|
# Adafruit MicroPython Tool - File Operations
|
||||||
|
# Author: Tony DiCola
|
||||||
|
# Copyright (c) 2016 Adafruit Industries
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
import ast
|
||||||
|
import textwrap
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ampy.pyboard import PyboardError
|
||||||
|
|
||||||
|
|
||||||
|
BUFFER_SIZE = 32 # Amount of data to read or write to the serial port at a time.
|
||||||
|
# This is kept small because small chips and USB to serial
|
||||||
|
# bridges usually have very small buffers.
|
||||||
|
|
||||||
|
|
||||||
|
class DirectoryExistsError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Files(object):
|
||||||
|
"""Class to interact with a MicroPython board files over a serial connection.
|
||||||
|
Provides functions for listing, uploading, and downloading files from the
|
||||||
|
board's filesystem.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, pyboard):
|
||||||
|
"""Initialize the MicroPython board files class using the provided pyboard
|
||||||
|
instance. In most cases you should create a Pyboard instance (from
|
||||||
|
pyboard.py) which connects to a board over a serial connection and pass
|
||||||
|
it in, but you can pass in other objects for testing, etc.
|
||||||
|
"""
|
||||||
|
self._pyboard = pyboard
|
||||||
|
|
||||||
|
def get(self, filename):
|
||||||
|
"""Retrieve the contents of the specified file and return its contents
|
||||||
|
as a byte string.
|
||||||
|
"""
|
||||||
|
# Open the file and read it a few bytes at a time and print out the
|
||||||
|
# raw bytes. Be careful not to overload the UART buffer so only write
|
||||||
|
# a few bytes at a time, and don't use print since it adds newlines and
|
||||||
|
# expects string data.
|
||||||
|
command = """
|
||||||
|
import sys
|
||||||
|
import ubinascii
|
||||||
|
with open('{0}', 'rb') as infile:
|
||||||
|
while True:
|
||||||
|
result = infile.read({1})
|
||||||
|
if result == b'':
|
||||||
|
break
|
||||||
|
len = sys.stdout.write(ubinascii.hexlify(result))
|
||||||
|
""".format(
|
||||||
|
filename, BUFFER_SIZE
|
||||||
|
)
|
||||||
|
self._pyboard.enter_raw_repl()
|
||||||
|
try:
|
||||||
|
out = self._pyboard.exec_(textwrap.dedent(command))
|
||||||
|
except PyboardError as ex:
|
||||||
|
# Check if this is an OSError #2, i.e. file doesn't exist and
|
||||||
|
# rethrow it as something more descriptive.
|
||||||
|
try:
|
||||||
|
if ex.args[2].decode("utf-8").find("OSError: [Errno 2] ENOENT") != -1:
|
||||||
|
raise RuntimeError("No such file: {0}".format(filename))
|
||||||
|
else:
|
||||||
|
raise ex
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
raise ex
|
||||||
|
self._pyboard.exit_raw_repl()
|
||||||
|
return out
|
||||||
|
|
||||||
|
def ls(self, directory="/", long_format=True, recursive=False, exit_repl=True):
|
||||||
|
"""List the contents of the specified directory (or root if none is
|
||||||
|
specified). Returns a list of strings with the names of files in the
|
||||||
|
specified directory. If long_format is True then a list of 2-tuples
|
||||||
|
with the name and size (in bytes) of the item is returned. Note that
|
||||||
|
it appears the size of directories is not supported by MicroPython and
|
||||||
|
will always return 0 (i.e. no recursive size computation).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Disabling for now, see https://github.com/adafruit/ampy/issues/55.
|
||||||
|
# # Make sure directory ends in a slash.
|
||||||
|
# if not directory.endswith("/"):
|
||||||
|
# directory += "/"
|
||||||
|
|
||||||
|
# Make sure directory starts with slash, for consistency.
|
||||||
|
if not directory.startswith("/"):
|
||||||
|
directory = "/" + directory
|
||||||
|
|
||||||
|
command = """\
|
||||||
|
try:
|
||||||
|
import os
|
||||||
|
except ImportError:
|
||||||
|
import uos as os\n"""
|
||||||
|
|
||||||
|
if recursive:
|
||||||
|
command += """\
|
||||||
|
def listdir(directory):
|
||||||
|
result = set()
|
||||||
|
|
||||||
|
def _listdir(dir_or_file):
|
||||||
|
try:
|
||||||
|
# if its a directory, then it should provide some children.
|
||||||
|
children = os.listdir(dir_or_file)
|
||||||
|
except OSError:
|
||||||
|
# probably a file. run stat() to confirm.
|
||||||
|
os.stat(dir_or_file)
|
||||||
|
result.add(dir_or_file)
|
||||||
|
else:
|
||||||
|
# probably a directory, add to result if empty.
|
||||||
|
if children:
|
||||||
|
# queue the children to be dealt with in next iteration.
|
||||||
|
for child in children:
|
||||||
|
# create the full path.
|
||||||
|
if dir_or_file == '/':
|
||||||
|
next = dir_or_file + child
|
||||||
|
else:
|
||||||
|
next = dir_or_file + '/' + child
|
||||||
|
|
||||||
|
_listdir(next)
|
||||||
|
else:
|
||||||
|
result.add(dir_or_file)
|
||||||
|
|
||||||
|
_listdir(directory)
|
||||||
|
return sorted(result)\n"""
|
||||||
|
else:
|
||||||
|
command += """\
|
||||||
|
def check_path(path):
|
||||||
|
try:
|
||||||
|
stat = os.stat(path)
|
||||||
|
# The first element of stat contains the file type and permission information
|
||||||
|
# The mode index of the tuple returned by os.stat() is 0
|
||||||
|
mode = stat[0]
|
||||||
|
# To determine whether it is a directory, check the directory bit in stat mode
|
||||||
|
if mode & 0o170000 == 0o040000:
|
||||||
|
if len(os.listdir(path)):
|
||||||
|
return 'dir'
|
||||||
|
else:
|
||||||
|
return 'empty dir'
|
||||||
|
# To determine whether it is a file, check the file position in stat mode
|
||||||
|
elif mode & 0o170000 == 0o100000:
|
||||||
|
return 'file'
|
||||||
|
else:
|
||||||
|
return 'special file'
|
||||||
|
except OSError:
|
||||||
|
return 'none'
|
||||||
|
|
||||||
|
def listdir(directory):
|
||||||
|
output = []
|
||||||
|
if directory == '/':
|
||||||
|
dirs = sorted([directory + f for f in os.listdir(directory)])
|
||||||
|
else:
|
||||||
|
dirs = sorted([directory + '/' + f for f in os.listdir(directory)])
|
||||||
|
|
||||||
|
for dir in dirs:
|
||||||
|
info = check_path(dir)
|
||||||
|
if info == 'none':
|
||||||
|
continue
|
||||||
|
output.append([dir, info])
|
||||||
|
return output\n"""
|
||||||
|
|
||||||
|
# Execute os.listdir() command on the board.
|
||||||
|
if long_format:
|
||||||
|
command += """
|
||||||
|
r = []
|
||||||
|
for f in listdir('{0}'):
|
||||||
|
size = os.stat(f)[6]
|
||||||
|
r.append('{{0}} - {{1}} bytes'.format(f, size))
|
||||||
|
print(r)
|
||||||
|
""".format(
|
||||||
|
directory
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
command += """
|
||||||
|
print(listdir('{0}'))
|
||||||
|
""".format(
|
||||||
|
directory
|
||||||
|
)
|
||||||
|
self._pyboard.enter_raw_repl()
|
||||||
|
try:
|
||||||
|
out = self._pyboard.exec_(textwrap.dedent(command))
|
||||||
|
except PyboardError as ex:
|
||||||
|
# Check if this is an OSError #2, i.e. directory doesn't exist and
|
||||||
|
# rethrow it as something more descriptive.
|
||||||
|
if ex.args[2].decode("utf-8").find("OSError: [Errno 2] ENOENT") != -1:
|
||||||
|
raise RuntimeError("No such directory: {0}".format(directory))
|
||||||
|
else:
|
||||||
|
raise ex
|
||||||
|
if exit_repl:
|
||||||
|
self._pyboard.exit_raw_repl()
|
||||||
|
# Parse the result list and return it.
|
||||||
|
return ast.literal_eval(out.decode("utf-8"))
|
||||||
|
|
||||||
|
def getFilesInfo(self, directory="/", recursive=False):
|
||||||
|
"""List the contents of the specified directory (or root if none is
|
||||||
|
specified). Returns a list of strings with the names of files in the
|
||||||
|
specified directory. If long_format is True then a list of 2-tuples
|
||||||
|
with the name and size (in bytes) of the item is returned. Note that
|
||||||
|
it appears the size of directories is not supported by MicroPython and
|
||||||
|
will always return 0 (i.e. no recursive size computation).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Disabling for now, see https://github.com/adafruit/ampy/issues/55.
|
||||||
|
# # Make sure directory ends in a slash.
|
||||||
|
# if not directory.endswith("/"):
|
||||||
|
# directory += "/"
|
||||||
|
|
||||||
|
# Make sure directory starts with slash, for consistency.
|
||||||
|
# if not directory.startswith("/"):
|
||||||
|
# directory = "/" + directory
|
||||||
|
|
||||||
|
command = """\
|
||||||
|
try:
|
||||||
|
import os
|
||||||
|
except ImportError:
|
||||||
|
import uos as os\n"""
|
||||||
|
|
||||||
|
if recursive:
|
||||||
|
command += """\
|
||||||
|
def listdir(directory):
|
||||||
|
result = set()
|
||||||
|
|
||||||
|
def _listdir(dir_or_file):
|
||||||
|
try:
|
||||||
|
# if its a directory, then it should provide some children.
|
||||||
|
children = os.listdir(dir_or_file)
|
||||||
|
except OSError:
|
||||||
|
# probably a file. run stat() to confirm.
|
||||||
|
os.stat(dir_or_file)
|
||||||
|
result.add(dir_or_file)
|
||||||
|
else:
|
||||||
|
# probably a directory, add to result if empty.
|
||||||
|
if children:
|
||||||
|
# queue the children to be dealt with in next iteration.
|
||||||
|
for child in children:
|
||||||
|
# create the full path.
|
||||||
|
if dir_or_file == '/':
|
||||||
|
next = dir_or_file + child
|
||||||
|
else:
|
||||||
|
next = dir_or_file + '/' + child
|
||||||
|
|
||||||
|
_listdir(next)
|
||||||
|
else:
|
||||||
|
result.add(dir_or_file)
|
||||||
|
|
||||||
|
_listdir(directory)
|
||||||
|
return sorted(result)\n"""
|
||||||
|
else:
|
||||||
|
command += """\
|
||||||
|
def listdir(directory):
|
||||||
|
try:
|
||||||
|
if directory == '/':
|
||||||
|
return sorted([directory + f for f in os.listdir(directory)])
|
||||||
|
else:
|
||||||
|
return sorted([directory + '/' + f for f in os.listdir(directory)])
|
||||||
|
except:
|
||||||
|
return sorted([f for f in os.listdir()])\n"""
|
||||||
|
|
||||||
|
# Execute os.listdir() command on the board.
|
||||||
|
# 当command执行出错时执行command1
|
||||||
|
command2 = command
|
||||||
|
command2 += """
|
||||||
|
r = []
|
||||||
|
for f in listdir('{0}'):
|
||||||
|
try:
|
||||||
|
size = os.stat(f)[6]
|
||||||
|
except:
|
||||||
|
size = os.size(f)
|
||||||
|
r.append([f, size])
|
||||||
|
print(r)
|
||||||
|
""".format(
|
||||||
|
(directory if directory else "/")
|
||||||
|
)
|
||||||
|
command += """
|
||||||
|
r = []
|
||||||
|
for f in listdir('{0}'):
|
||||||
|
try:
|
||||||
|
size = os.stat(f)[6]
|
||||||
|
except:
|
||||||
|
size = os.size(f)
|
||||||
|
r.append([f, size])
|
||||||
|
print(r)
|
||||||
|
""".format(
|
||||||
|
directory
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
out = self._pyboard.exec_(textwrap.dedent(command))
|
||||||
|
except PyboardError as ex:
|
||||||
|
out = self._pyboard.exec_(textwrap.dedent(command2))
|
||||||
|
# Check if this is an OSError #2, i.e. directory doesn't exist and
|
||||||
|
# rethrow it as something more descriptive.
|
||||||
|
except PyboardError as ex:
|
||||||
|
if ex.args[2].decode("utf-8").find("OSError: [Errno 2] ENOENT") != -1:
|
||||||
|
raise RuntimeError("No such directory: {0}".format(directory))
|
||||||
|
else:
|
||||||
|
raise ex
|
||||||
|
# Parse the result list and return it.
|
||||||
|
try:
|
||||||
|
return ast.literal_eval(out.decode("utf-8"))
|
||||||
|
except:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def mkdir(self, directory, exists_okay=False):
|
||||||
|
"""Create the specified directory. Note this cannot create a recursive
|
||||||
|
hierarchy of directories, instead each one should be created separately.
|
||||||
|
"""
|
||||||
|
# Execute os.mkdir command on the board.
|
||||||
|
command = """
|
||||||
|
try:
|
||||||
|
import os
|
||||||
|
except ImportError:
|
||||||
|
import uos as os
|
||||||
|
os.mkdir('{0}')
|
||||||
|
""".format(
|
||||||
|
directory
|
||||||
|
)
|
||||||
|
self._pyboard.enter_raw_repl()
|
||||||
|
try:
|
||||||
|
out = self._pyboard.exec_(textwrap.dedent(command))
|
||||||
|
except PyboardError as ex:
|
||||||
|
# Check if this is an OSError #17, i.e. directory already exists.
|
||||||
|
if ex.args[2].decode("utf-8").find("OSError: [Errno 17] EEXIST") != -1:
|
||||||
|
if not exists_okay:
|
||||||
|
raise DirectoryExistsError(
|
||||||
|
"Directory already exists: {0}".format(directory)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ex
|
||||||
|
self._pyboard.exit_raw_repl()
|
||||||
|
|
||||||
|
def mkfile(self, file, exists_okay=False):
|
||||||
|
command = """
|
||||||
|
try:
|
||||||
|
import os
|
||||||
|
except ImportError:
|
||||||
|
import uos as os
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.stat('{0}')
|
||||||
|
except OSError:
|
||||||
|
f = open('{0}', 'w')
|
||||||
|
f.close()
|
||||||
|
""".format(
|
||||||
|
file
|
||||||
|
)
|
||||||
|
self._pyboard.enter_raw_repl()
|
||||||
|
try:
|
||||||
|
out = self._pyboard.exec_(textwrap.dedent(command))
|
||||||
|
except PyboardError as ex:
|
||||||
|
raise ex
|
||||||
|
self._pyboard.exit_raw_repl()
|
||||||
|
|
||||||
|
def put(self, filename, data, enter_repl=True, exit_repl=True):
|
||||||
|
"""Create or update the specified file with the provided data.
|
||||||
|
"""
|
||||||
|
# Open the file for writing on the board and write chunks of data.
|
||||||
|
if enter_repl:
|
||||||
|
self._pyboard.enter_raw_repl()
|
||||||
|
self._pyboard.exec_("f = open('{0}', 'wb')".format(filename))
|
||||||
|
sys.stdout.write("Write " + filename)
|
||||||
|
size = len(data)
|
||||||
|
# Loop through and write a buffer size chunk of data at a time.
|
||||||
|
for i in range(0, size, BUFFER_SIZE):
|
||||||
|
chunk_size = min(BUFFER_SIZE, size - i)
|
||||||
|
chunk = repr(data[i : i + chunk_size])
|
||||||
|
# Make sure to send explicit byte strings (handles python 2 compatibility).
|
||||||
|
if not chunk.startswith("b"):
|
||||||
|
chunk = "b" + chunk
|
||||||
|
self._pyboard.exec_("f.write({0})".format(chunk))
|
||||||
|
self._pyboard.exec_("f.close()")
|
||||||
|
sys.stdout.write(" Done!\n")
|
||||||
|
if exit_repl:
|
||||||
|
self._pyboard.exit_raw_repl()
|
||||||
|
|
||||||
|
def putdir(self, fileNameList, dataList, enter_repl=True, exit_repl=True):
|
||||||
|
"""Create or update the specified file with the provided data.
|
||||||
|
"""
|
||||||
|
# Open the file for writing on the board and write chunks of data.
|
||||||
|
if enter_repl:
|
||||||
|
self._pyboard.enter_raw_repl()
|
||||||
|
for i in range(0, len(fileNameList), 1):
|
||||||
|
self._pyboard.exec_("f = open('{0}', 'wb')".format(fileNameList[i]))
|
||||||
|
sys.stdout.write("Writing " + fileNameList[i])
|
||||||
|
sys.stdout.flush()
|
||||||
|
#print("write " + fileNameList[i])
|
||||||
|
data = dataList[i]
|
||||||
|
size = len(data)
|
||||||
|
# Loop through and write a buffer size chunk of data at a time.
|
||||||
|
for i in range(0, size, BUFFER_SIZE):
|
||||||
|
chunk_size = min(BUFFER_SIZE, size - i)
|
||||||
|
chunk = repr(data[i : i + chunk_size])
|
||||||
|
# Make sure to send explicit byte strings (handles python 2 compatibility).
|
||||||
|
if not chunk.startswith("b"):
|
||||||
|
chunk = "b" + chunk
|
||||||
|
self._pyboard.exec_("f.write({0})".format(chunk))
|
||||||
|
self._pyboard.exec_("f.close()")
|
||||||
|
sys.stdout.write(" Done!\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
if exit_repl:
|
||||||
|
self._pyboard.exit_raw_repl()
|
||||||
|
|
||||||
|
def rm(self, filename):
|
||||||
|
"""Remove the specified file or directory."""
|
||||||
|
command = """
|
||||||
|
try:
|
||||||
|
import os
|
||||||
|
except ImportError:
|
||||||
|
import uos as os
|
||||||
|
os.remove('{0}')
|
||||||
|
""".format(
|
||||||
|
filename
|
||||||
|
)
|
||||||
|
self._pyboard.enter_raw_repl()
|
||||||
|
try:
|
||||||
|
out = self._pyboard.exec_(textwrap.dedent(command))
|
||||||
|
except PyboardError as ex:
|
||||||
|
message = ex.args[2].decode("utf-8")
|
||||||
|
# Check if this is an OSError #2, i.e. file/directory doesn't exist
|
||||||
|
# and rethrow it as something more descriptive.
|
||||||
|
if message.find("OSError: [Errno 2] ENOENT") != -1:
|
||||||
|
raise RuntimeError("No such file/directory: {0}".format(filename))
|
||||||
|
# Check for OSError #13, the directory isn't empty.
|
||||||
|
if message.find("OSError: [Errno 13] EACCES") != -1:
|
||||||
|
raise RuntimeError("Directory is not empty: {0}".format(filename))
|
||||||
|
else:
|
||||||
|
raise ex
|
||||||
|
self._pyboard.exit_raw_repl()
|
||||||
|
|
||||||
|
def rmdir(self, directory, missing_okay=False):
|
||||||
|
"""Forcefully remove the specified directory and all its children."""
|
||||||
|
# Build a script to walk an entire directory structure and delete every
|
||||||
|
# file and subfolder. This is tricky because MicroPython has no os.walk
|
||||||
|
# or similar function to walk folders, so this code does it manually
|
||||||
|
# with recursion and changing directories. For each directory it lists
|
||||||
|
# the files and deletes everything it can, i.e. all the files. Then
|
||||||
|
# it lists the files again and assumes they are directories (since they
|
||||||
|
# couldn't be deleted in the first pass) and recursively clears those
|
||||||
|
# subdirectories. Finally when finished clearing all the children the
|
||||||
|
# parent directory is deleted.
|
||||||
|
command = """
|
||||||
|
try:
|
||||||
|
import os
|
||||||
|
except ImportError:
|
||||||
|
import uos as os
|
||||||
|
|
||||||
|
def rmdir(directory):
|
||||||
|
os.chdir(directory)
|
||||||
|
for f in os.listdir():
|
||||||
|
try:
|
||||||
|
os.remove(f)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
for f in os.listdir():
|
||||||
|
rmdir(f)
|
||||||
|
os.chdir('..')
|
||||||
|
os.rmdir(directory)
|
||||||
|
rmdir('{0}')
|
||||||
|
""".format(
|
||||||
|
directory
|
||||||
|
)
|
||||||
|
self._pyboard.enter_raw_repl()
|
||||||
|
try:
|
||||||
|
out = self._pyboard.exec_(textwrap.dedent(command))
|
||||||
|
except PyboardError as ex:
|
||||||
|
message = ex.args[2].decode("utf-8")
|
||||||
|
# Check if this is an OSError #2, i.e. directory doesn't exist
|
||||||
|
# and rethrow it as something more descriptive.
|
||||||
|
if message.find("OSError: [Errno 2] ENOENT") != -1:
|
||||||
|
if not missing_okay:
|
||||||
|
raise RuntimeError("No such directory: {0}".format(directory))
|
||||||
|
else:
|
||||||
|
raise ex
|
||||||
|
self._pyboard.exit_raw_repl()
|
||||||
|
|
||||||
|
def rename(self, oldname, newname):
|
||||||
|
command = """
|
||||||
|
try:
|
||||||
|
import os
|
||||||
|
except ImportError:
|
||||||
|
import uos as os
|
||||||
|
|
||||||
|
os.rename('{0}', '{1}')
|
||||||
|
""".format(
|
||||||
|
oldname, newname
|
||||||
|
)
|
||||||
|
self._pyboard.enter_raw_repl()
|
||||||
|
try:
|
||||||
|
out = self._pyboard.exec_(textwrap.dedent(command))
|
||||||
|
except PyboardError as ex:
|
||||||
|
message = ex.args[2].decode("utf-8")
|
||||||
|
raise ex
|
||||||
|
self._pyboard.exit_raw_repl()
|
||||||
|
|
||||||
|
def cpdir(self, oldpath, newpath):
|
||||||
|
command = """
|
||||||
|
try:
|
||||||
|
import os
|
||||||
|
except ImportError:
|
||||||
|
import uos as os
|
||||||
|
|
||||||
|
def cpfile(src, dst):
|
||||||
|
with open(src, 'rb') as src_file:
|
||||||
|
content = src_file.read()
|
||||||
|
with open(dst, 'wb') as dst_file:
|
||||||
|
dst_file.write(content)
|
||||||
|
|
||||||
|
def cpdir(src, dst):
|
||||||
|
try:
|
||||||
|
os.mkdir(dst)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
for item in os.listdir(src):
|
||||||
|
src_path = src + '/' + item
|
||||||
|
dst_path = dst + '/' + item
|
||||||
|
stat = os.stat(src_path)
|
||||||
|
mode = stat[0]
|
||||||
|
if mode & 0o170000 == 0o040000:
|
||||||
|
cpdir(src_path, dst_path)
|
||||||
|
else:
|
||||||
|
cpfile(src_path, dst_path)
|
||||||
|
|
||||||
|
cpdir('{0}', '{1}')
|
||||||
|
""".format(
|
||||||
|
oldpath, newpath
|
||||||
|
)
|
||||||
|
self._pyboard.enter_raw_repl()
|
||||||
|
try:
|
||||||
|
out = self._pyboard.exec_(textwrap.dedent(command))
|
||||||
|
except PyboardError as ex:
|
||||||
|
message = ex.args[2].decode("utf-8")
|
||||||
|
raise ex
|
||||||
|
self._pyboard.exit_raw_repl()
|
||||||
|
|
||||||
|
def cpfile(self, oldpath, newpath):
|
||||||
|
command = """
|
||||||
|
try:
|
||||||
|
import os
|
||||||
|
except ImportError:
|
||||||
|
import uos as os
|
||||||
|
def cpfile(src, dst):
|
||||||
|
with open(src, 'rb') as src_file:
|
||||||
|
content = src_file.read()
|
||||||
|
with open(dst, 'wb') as dst_file:
|
||||||
|
dst_file.write(content)
|
||||||
|
|
||||||
|
cpfile('{0}', '{1}')
|
||||||
|
""".format(
|
||||||
|
oldpath, newpath
|
||||||
|
)
|
||||||
|
self._pyboard.enter_raw_repl()
|
||||||
|
try:
|
||||||
|
out = self._pyboard.exec_(textwrap.dedent(command))
|
||||||
|
except PyboardError as ex:
|
||||||
|
message = ex.args[2].decode("utf-8")
|
||||||
|
raise ex
|
||||||
|
self._pyboard.exit_raw_repl()
|
||||||
|
|
||||||
|
def run(self, filename, wait_output=True, stream_output=True):
|
||||||
|
"""Run the provided script and return its output. If wait_output is True
|
||||||
|
(default) then wait for the script to finish and then return its output,
|
||||||
|
otherwise just run the script and don't wait for any output.
|
||||||
|
If stream_output is True(default) then return None and print outputs to
|
||||||
|
stdout without buffering.
|
||||||
|
"""
|
||||||
|
self._pyboard.enter_raw_repl()
|
||||||
|
out = None
|
||||||
|
if stream_output:
|
||||||
|
self._pyboard.execfile(filename, stream_output=True)
|
||||||
|
elif wait_output:
|
||||||
|
# Run the file and wait for output to return.
|
||||||
|
out = self._pyboard.execfile(filename)
|
||||||
|
else:
|
||||||
|
# Read the file and run it using lower level pyboard functions that
|
||||||
|
# won't wait for it to finish or return output.
|
||||||
|
with open(filename, "rb") as infile:
|
||||||
|
self._pyboard.exec_raw_no_follow(infile.read())
|
||||||
|
self._pyboard.exit_raw_repl()
|
||||||
|
return out
|
||||||
448
mixly/tools/python/ampy/pyboard.py
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
"""
|
||||||
|
pyboard interface
|
||||||
|
|
||||||
|
This module provides the Pyboard class, used to communicate with and
|
||||||
|
control the pyboard over a serial USB connection.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
import pyboard
|
||||||
|
pyb = pyboard.Pyboard('/dev/ttyACM0')
|
||||||
|
|
||||||
|
Or:
|
||||||
|
|
||||||
|
pyb = pyboard.Pyboard('192.168.1.1')
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
pyb.enter_raw_repl()
|
||||||
|
pyb.exec('pyb.LED(1).on()')
|
||||||
|
pyb.exit_raw_repl()
|
||||||
|
|
||||||
|
Note: if using Python2 then pyb.exec must be written as pyb.exec_.
|
||||||
|
To run a script from the local machine on the board and print out the results:
|
||||||
|
|
||||||
|
import pyboard
|
||||||
|
pyboard.execfile('test.py', device='/dev/ttyACM0')
|
||||||
|
|
||||||
|
This script can also be run directly. To execute a local script, use:
|
||||||
|
|
||||||
|
./pyboard.py test.py
|
||||||
|
|
||||||
|
Or:
|
||||||
|
|
||||||
|
python pyboard.py test.py
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
|
_rawdelay = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
stdout = sys.stdout.buffer
|
||||||
|
except AttributeError:
|
||||||
|
# Python2 doesn't have buffer attr
|
||||||
|
stdout = sys.stdout
|
||||||
|
|
||||||
|
def stdout_write_bytes(b):
|
||||||
|
b = b.replace(b"\x04", b"")
|
||||||
|
stdout.write(b)
|
||||||
|
stdout.flush()
|
||||||
|
|
||||||
|
class PyboardError(BaseException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TelnetToSerial:
|
||||||
|
def __init__(self, ip, user, password, read_timeout=None):
|
||||||
|
import telnetlib
|
||||||
|
self.tn = telnetlib.Telnet(ip, timeout=15)
|
||||||
|
self.read_timeout = read_timeout
|
||||||
|
if b'Login as:' in self.tn.read_until(b'Login as:', timeout=read_timeout):
|
||||||
|
self.tn.write(bytes(user, 'ascii') + b"\r\n")
|
||||||
|
|
||||||
|
if b'Password:' in self.tn.read_until(b'Password:', timeout=read_timeout):
|
||||||
|
# needed because of internal implementation details of the telnet server
|
||||||
|
time.sleep(0.2)
|
||||||
|
self.tn.write(bytes(password, 'ascii') + b"\r\n")
|
||||||
|
|
||||||
|
if b'for more information.' in self.tn.read_until(b'Type "help()" for more information.', timeout=read_timeout):
|
||||||
|
# login succesful
|
||||||
|
from collections import deque
|
||||||
|
self.fifo = deque()
|
||||||
|
return
|
||||||
|
|
||||||
|
raise PyboardError('Failed to establish a telnet connection with the board')
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
try:
|
||||||
|
self.tn.close()
|
||||||
|
except:
|
||||||
|
# the telnet object might not exist yet, so ignore this one
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read(self, size=1):
|
||||||
|
while len(self.fifo) < size:
|
||||||
|
timeout_count = 0
|
||||||
|
data = self.tn.read_eager()
|
||||||
|
if len(data):
|
||||||
|
self.fifo.extend(data)
|
||||||
|
timeout_count = 0
|
||||||
|
else:
|
||||||
|
time.sleep(0.25)
|
||||||
|
if self.read_timeout is not None and timeout_count > 4 * self.read_timeout:
|
||||||
|
break
|
||||||
|
timeout_count += 1
|
||||||
|
|
||||||
|
data = b''
|
||||||
|
while len(data) < size and len(self.fifo) > 0:
|
||||||
|
data += bytes([self.fifo.popleft()])
|
||||||
|
return data
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
self.tn.write(data)
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
def inWaiting(self):
|
||||||
|
n_waiting = len(self.fifo)
|
||||||
|
if not n_waiting:
|
||||||
|
data = self.tn.read_eager()
|
||||||
|
self.fifo.extend(data)
|
||||||
|
return len(data)
|
||||||
|
else:
|
||||||
|
return n_waiting
|
||||||
|
|
||||||
|
class Pyboard:
|
||||||
|
def __init__(self, device, baudrate=115200, user='micro', password='python', wait=0, rawdelay=0, boardreset="{}", file_empty="main.py", info=True):
|
||||||
|
self.boardreset = json.loads(boardreset);
|
||||||
|
self.file_empty = file_empty;
|
||||||
|
self.info = info;
|
||||||
|
global _rawdelay
|
||||||
|
_rawdelay = rawdelay
|
||||||
|
if device and device[0].isdigit() and device[-1].isdigit() and device.count('.') == 3:
|
||||||
|
# device looks like an IP address
|
||||||
|
self.serial = TelnetToSerial(device, user, password, read_timeout=10)
|
||||||
|
else:
|
||||||
|
import serial
|
||||||
|
delayed = False
|
||||||
|
for attempt in range(wait + 1):
|
||||||
|
try:
|
||||||
|
self.serial = serial.Serial(device, baudrate=baudrate, interCharTimeout=10, writeTimeout=5, timeout=40)
|
||||||
|
break
|
||||||
|
except (OSError, IOError): # Py2 and Py3 have different errors
|
||||||
|
if wait == 0:
|
||||||
|
continue
|
||||||
|
if attempt == 0:
|
||||||
|
sys.stdout.write('Waiting {} seconds for pyboard '.format(wait))
|
||||||
|
delayed = True
|
||||||
|
time.sleep(1)
|
||||||
|
#sys.stdout.write('.')
|
||||||
|
#sys.stdout.flush()
|
||||||
|
else:
|
||||||
|
if delayed:
|
||||||
|
print('')
|
||||||
|
raise PyboardError('failed to access ' + device)
|
||||||
|
if delayed:
|
||||||
|
print('')
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.serial.close()
|
||||||
|
|
||||||
|
def read_until(self, min_num_bytes, ending, timeout=1, data_consumer=None):
|
||||||
|
data = self.serial.read(min_num_bytes)
|
||||||
|
if data_consumer:
|
||||||
|
data_consumer(data)
|
||||||
|
timeout_count = 0
|
||||||
|
while True:
|
||||||
|
if data.endswith(ending) or data.lower().endswith(ending):
|
||||||
|
break
|
||||||
|
elif self.serial.inWaiting() > 0:
|
||||||
|
new_data = self.serial.read(1)
|
||||||
|
data = data + new_data
|
||||||
|
if data_consumer:
|
||||||
|
data_consumer(new_data)
|
||||||
|
timeout_count = 0
|
||||||
|
else:
|
||||||
|
timeout_count += 1
|
||||||
|
if timeout is not None and timeout_count >= 10 * timeout:
|
||||||
|
break
|
||||||
|
time.sleep(0.01)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def enter_raw_repl(self):
|
||||||
|
# Brief delay before sending RAW MODE char if requests
|
||||||
|
if _rawdelay > 0:
|
||||||
|
time.sleep(_rawdelay)
|
||||||
|
# ctrl-C twice: interrupt any running program
|
||||||
|
#sys.stdout.write("Try to delete ./{} ".format(self.file_empty))
|
||||||
|
if self.info:
|
||||||
|
sys.stdout.write("Try to enter REPL ")
|
||||||
|
sys.stdout.flush()
|
||||||
|
repl_ok=False
|
||||||
|
oldInterCharTimeout = self.serial.interCharTimeout
|
||||||
|
oldTimeout = self.serial.timeout
|
||||||
|
self.serial.interCharTimeout = 1
|
||||||
|
self.serial.timeout = 1
|
||||||
|
try:
|
||||||
|
for retry in range(10):
|
||||||
|
self.serial.write(b'\r\x02\x03')
|
||||||
|
time.sleep(0.1)
|
||||||
|
self.serial.write(b'\x02\x03')
|
||||||
|
time.sleep(0.1)
|
||||||
|
data = self.read_until(1, b'>')
|
||||||
|
if self.info:
|
||||||
|
sys.stdout.write(".")
|
||||||
|
sys.stdout.flush()
|
||||||
|
if data.endswith(b'>'):
|
||||||
|
repl_ok=True
|
||||||
|
if self.info:
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
sys.stdout.write("Delete {} ".format(self.file_empty))
|
||||||
|
sys.stdout.flush()
|
||||||
|
self.serial.write(bytes("import os; os.remove('{}')\r\n".format(self.file_empty), encoding="utf8"))
|
||||||
|
time.sleep(0.1)
|
||||||
|
if self.info:
|
||||||
|
sys.stdout.write("Done!\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
break
|
||||||
|
if retry >8:
|
||||||
|
if self.info:
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
sys.stdout.write('could not enter raw repl, Try to reset\n')
|
||||||
|
sys.stdout.flush()
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
self.serial.interCharTimeout = oldInterCharTimeout
|
||||||
|
self.serial.timeout = oldTimeout
|
||||||
|
if not repl_ok:
|
||||||
|
if self.info:
|
||||||
|
sys.stdout.write("Reset Start\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
resetLen = len(self.boardreset)
|
||||||
|
if resetLen:
|
||||||
|
for i in range(0, resetLen, 1):
|
||||||
|
dtr = self.boardreset[i].get("dtr", -1)
|
||||||
|
rts = self.boardreset[i].get("rts", -1)
|
||||||
|
sleep = self.boardreset[i].get("sleep", -1)
|
||||||
|
if dtr != -1 and rts != -1:
|
||||||
|
self.serial.setDTR (dtr)
|
||||||
|
self.serial.setRTS (rts)
|
||||||
|
if self.info:
|
||||||
|
sys.stdout.write("Set dtr:{}, rts:{}\n".format(dtr, rts))
|
||||||
|
elif sleep != -1:
|
||||||
|
time.sleep(sleep / 1000)
|
||||||
|
if self.info:
|
||||||
|
sys.stdout.write("Set sleep:{}s\n".format(sleep / 1000))
|
||||||
|
if self.info:
|
||||||
|
sys.stdout.flush()
|
||||||
|
else:
|
||||||
|
self.serial.setDTR (False)
|
||||||
|
self.serial.setRTS (False)
|
||||||
|
if self.info:
|
||||||
|
sys.stdout.write("Set dtr:{}, rts:{}\n".format(False, False))
|
||||||
|
sys.stdout.flush()
|
||||||
|
time.sleep(0.1)
|
||||||
|
if self.info:
|
||||||
|
sys.stdout.write("Set sleep:{}s\n".format(0.1))
|
||||||
|
sys.stdout.flush()
|
||||||
|
self.serial.setDTR (True)
|
||||||
|
self.serial.setRTS (True)
|
||||||
|
if self.info:
|
||||||
|
sys.stdout.write("Set dtr:{}, rts:{}\n".format(True, True))
|
||||||
|
sys.stdout.flush()
|
||||||
|
time.sleep(0.1)
|
||||||
|
if self.info:
|
||||||
|
sys.stdout.write("Set sleep:{}s\n".format(0.1))
|
||||||
|
sys.stdout.flush()
|
||||||
|
if self.info:
|
||||||
|
sys.stdout.write("Reset Done!\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
# Brief delay before sending RAW MODE char if requests
|
||||||
|
if _rawdelay > 0:
|
||||||
|
time.sleep(_rawdelay)
|
||||||
|
# ctrl-C twice: interrupt any running program
|
||||||
|
self.serial.write(b'\r\x03')
|
||||||
|
time.sleep(0.1)
|
||||||
|
self.serial.write(b'\x03')
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# flush input (without relying on serial.flushInput())
|
||||||
|
n = self.serial.inWaiting()
|
||||||
|
while n > 0:
|
||||||
|
self.serial.read(n)
|
||||||
|
n = self.serial.inWaiting()
|
||||||
|
#time.sleep(2)
|
||||||
|
#self.serial.write(b'\x03\x04')
|
||||||
|
for retry in range(0, 5):
|
||||||
|
self.serial.write(b'\r\x01') # ctrl-A: enter raw REPL
|
||||||
|
data = self.read_until(1, b'raw REPL; CTRL-B to exit\r\n>')
|
||||||
|
if data.endswith(b'raw REPL; CTRL-B to exit\r\n>'):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if retry >= 4:
|
||||||
|
raise PyboardError('could not enter raw repl')
|
||||||
|
else:
|
||||||
|
self.serial.write(b'\r\x03')
|
||||||
|
time.sleep(0.1)
|
||||||
|
self.serial.write(b'\x03')
|
||||||
|
time.sleep(0.1)
|
||||||
|
self.serial.write(b'\x04') # ctrl-D: soft reset
|
||||||
|
data = self.read_until(1, b'soft reboot\r\n')
|
||||||
|
if not data.lower().endswith(b'soft reboot\r\n'):
|
||||||
|
self.serial.write(b'\x04') # ctrl-D: soft reset
|
||||||
|
raise PyboardError('could not enter raw repl')
|
||||||
|
# By splitting this into 2 reads, it allows boot.py to print stuff,
|
||||||
|
# which will show up after the soft reboot and before the raw REPL.
|
||||||
|
# Modification from original pyboard.py below:
|
||||||
|
# Add a small delay and send Ctrl-C twice after soft reboot to ensure
|
||||||
|
# any main program loop in main.py is interrupted.
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.serial.write(b'\x03')
|
||||||
|
time.sleep(0.1) # (slight delay before second interrupt
|
||||||
|
self.serial.write(b'\x03')
|
||||||
|
# End modification above.
|
||||||
|
data = self.read_until(1, b'raw REPL; CTRL-B to exit\r\n')
|
||||||
|
if not data.endswith(b'raw REPL; CTRL-B to exit\r\n'):
|
||||||
|
raise PyboardError('could not enter raw repl')
|
||||||
|
|
||||||
|
def exit_raw_repl(self):
|
||||||
|
self.serial.write(b'\r\x02') # ctrl-B: enter friendly REPL
|
||||||
|
|
||||||
|
def follow(self, timeout, data_consumer=None):
|
||||||
|
# wait for normal output
|
||||||
|
data = self.read_until(1, b'\x04', timeout=timeout, data_consumer=data_consumer)
|
||||||
|
if not data.endswith(b'\x04'):
|
||||||
|
raise PyboardError('timeout waiting for first EOF reception')
|
||||||
|
data = data[:-1]
|
||||||
|
|
||||||
|
# wait for error output
|
||||||
|
data_err = self.read_until(1, b'\x04', timeout=timeout)
|
||||||
|
if not data_err.endswith(b'\x04'):
|
||||||
|
raise PyboardError('timeout waiting for second EOF reception')
|
||||||
|
data_err = data_err[:-1]
|
||||||
|
|
||||||
|
# return normal and error output
|
||||||
|
return data, data_err
|
||||||
|
|
||||||
|
def exec_raw_no_follow(self, command):
|
||||||
|
if isinstance(command, bytes):
|
||||||
|
command_bytes = command
|
||||||
|
else:
|
||||||
|
command_bytes = bytes(command, encoding='utf8')
|
||||||
|
|
||||||
|
# check we have a prompt
|
||||||
|
data = self.read_until(1, b'>')
|
||||||
|
if not data.endswith(b'>'):
|
||||||
|
raise PyboardError('could not enter raw repl')
|
||||||
|
|
||||||
|
# write command
|
||||||
|
for i in range(0, len(command_bytes), 256):
|
||||||
|
self.serial.write(command_bytes[i:min(i + 256, len(command_bytes))])
|
||||||
|
time.sleep(0.01)
|
||||||
|
self.serial.write(b'\x04')
|
||||||
|
|
||||||
|
# check if we could exec command
|
||||||
|
data = self.serial.read(2)
|
||||||
|
if data != b'OK' and data != b'ra':
|
||||||
|
raise PyboardError('could not exec command')
|
||||||
|
|
||||||
|
def exec_raw(self, command, timeout=10, data_consumer=None):
|
||||||
|
self.exec_raw_no_follow(command);
|
||||||
|
return self.follow(timeout, data_consumer)
|
||||||
|
|
||||||
|
def eval(self, expression):
|
||||||
|
ret = self.exec_('print({})'.format(expression))
|
||||||
|
ret = ret.strip()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def exec_(self, command, stream_output=False):
|
||||||
|
data_consumer = None
|
||||||
|
if stream_output:
|
||||||
|
data_consumer = stdout_write_bytes
|
||||||
|
ret, ret_err = self.exec_raw(command, data_consumer=data_consumer)
|
||||||
|
if ret_err:
|
||||||
|
raise PyboardError('exception', ret, ret_err)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def execfile(self, filename, stream_output=False):
|
||||||
|
with open(filename, 'rb') as f:
|
||||||
|
pyfile = f.read()
|
||||||
|
return self.exec_(pyfile, stream_output=stream_output)
|
||||||
|
|
||||||
|
def get_time(self):
|
||||||
|
t = str(self.eval('pyb.RTC().datetime()'), encoding='utf8')[1:-1].split(', ')
|
||||||
|
return int(t[4]) * 3600 + int(t[5]) * 60 + int(t[6])
|
||||||
|
|
||||||
|
# in Python2 exec is a keyword so one must use "exec_"
|
||||||
|
# but for Python3 we want to provide the nicer version "exec"
|
||||||
|
setattr(Pyboard, "exec", Pyboard.exec_)
|
||||||
|
|
||||||
|
def execfile(filename, device='/dev/ttyACM0', baudrate=115200, user='micro', password='python'):
|
||||||
|
pyb = Pyboard(device, baudrate, user, password)
|
||||||
|
pyb.enter_raw_repl()
|
||||||
|
output = pyb.execfile(filename)
|
||||||
|
stdout_write_bytes(output)
|
||||||
|
pyb.exit_raw_repl()
|
||||||
|
pyb.close()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import argparse
|
||||||
|
cmd_parser = argparse.ArgumentParser(description='Run scripts on the pyboard.')
|
||||||
|
cmd_parser.add_argument('--device', default='/dev/ttyACM0', help='the serial device or the IP address of the pyboard')
|
||||||
|
cmd_parser.add_argument('-b', '--baudrate', default=115200, help='the baud rate of the serial device')
|
||||||
|
cmd_parser.add_argument('-u', '--user', default='micro', help='the telnet login username')
|
||||||
|
cmd_parser.add_argument('-p', '--password', default='python', help='the telnet login password')
|
||||||
|
cmd_parser.add_argument('-c', '--command', help='program passed in as string')
|
||||||
|
cmd_parser.add_argument('-w', '--wait', default=0, type=int, help='seconds to wait for USB connected board to become available')
|
||||||
|
cmd_parser.add_argument('--follow', action='store_true', help='follow the output after running the scripts [default if no scripts given]')
|
||||||
|
cmd_parser.add_argument('files', nargs='*', help='input files')
|
||||||
|
args = cmd_parser.parse_args()
|
||||||
|
|
||||||
|
def execbuffer(buf):
|
||||||
|
try:
|
||||||
|
pyb = Pyboard(args.device, args.baudrate, args.user, args.password, args.wait)
|
||||||
|
pyb.enter_raw_repl()
|
||||||
|
ret, ret_err = pyb.exec_raw(buf, timeout=None, data_consumer=stdout_write_bytes)
|
||||||
|
pyb.exit_raw_repl()
|
||||||
|
pyb.close()
|
||||||
|
except PyboardError as er:
|
||||||
|
print(er)
|
||||||
|
sys.exit(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.exit(1)
|
||||||
|
if ret_err:
|
||||||
|
stdout_write_bytes(ret_err)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if args.command is not None:
|
||||||
|
execbuffer(args.command.encode('utf-8'))
|
||||||
|
|
||||||
|
for filename in args.files:
|
||||||
|
with open(filename, 'rb') as f:
|
||||||
|
pyfile = f.read()
|
||||||
|
execbuffer(pyfile)
|
||||||
|
|
||||||
|
if args.follow or (args.command is None and len(args.files) == 0):
|
||||||
|
try:
|
||||||
|
pyb = Pyboard(args.device, args.baudrate, args.user, args.password, args.wait)
|
||||||
|
ret, ret_err = pyb.follow(timeout=None, data_consumer=stdout_write_bytes)
|
||||||
|
pyb.close()
|
||||||
|
except PyboardError as er:
|
||||||
|
print(er)
|
||||||
|
sys.exit(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.exit(1)
|
||||||
|
if ret_err:
|
||||||
|
stdout_write_bytes(ret_err)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
24
mixly/tools/python/ampy_main.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import sys
|
||||||
|
from ampy.cli import cli, _board
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
error_exit = False
|
||||||
|
try:
|
||||||
|
cli()
|
||||||
|
except BaseException as e:
|
||||||
|
if getattr(e, 'code', True):
|
||||||
|
print('Error: {}'.format(e))
|
||||||
|
error_exit = True
|
||||||
|
finally:
|
||||||
|
# Try to ensure the board serial connection is always gracefully closed.
|
||||||
|
if _board is not None:
|
||||||
|
try:
|
||||||
|
_board.close()
|
||||||
|
except:
|
||||||
|
# Swallow errors when attempting to close as it's just a best effort
|
||||||
|
# and shouldn't cause a new error or problem if the connection can't
|
||||||
|
# be closed.
|
||||||
|
pass
|
||||||
|
if error_exit:
|
||||||
|
sys.exit(1)
|
||||||
4
mixly/tools/python/backports/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# See https://pypi.python.org/pypi/backports
|
||||||
|
|
||||||
|
from pkgutil import extend_path
|
||||||
|
__path__ = extend_path(__path__, __name__)
|
||||||
75
mixly/tools/python/backports/tempfile.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
"""
|
||||||
|
Partial backport of Python 3.5's tempfile module:
|
||||||
|
|
||||||
|
TemporaryDirectory
|
||||||
|
|
||||||
|
Backport modifications are marked with marked with "XXX backport".
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import warnings as _warnings
|
||||||
|
from shutil import rmtree as _rmtree
|
||||||
|
|
||||||
|
from backports.weakref import finalize
|
||||||
|
|
||||||
|
|
||||||
|
# XXX backport: Rather than backporting all of mkdtemp(), we just create a
|
||||||
|
# thin wrapper implementing its Python 3.5 signature.
|
||||||
|
if sys.version_info < (3, 5):
|
||||||
|
from tempfile import mkdtemp as old_mkdtemp
|
||||||
|
|
||||||
|
def mkdtemp(suffix=None, prefix=None, dir=None):
|
||||||
|
"""
|
||||||
|
Wrap `tempfile.mkdtemp()` to make the suffix and prefix optional (like Python 3.5).
|
||||||
|
"""
|
||||||
|
kwargs = {k: v for (k, v) in
|
||||||
|
dict(suffix=suffix, prefix=prefix, dir=dir).items()
|
||||||
|
if v is not None}
|
||||||
|
return old_mkdtemp(**kwargs)
|
||||||
|
|
||||||
|
else:
|
||||||
|
from tempfile import mkdtemp
|
||||||
|
|
||||||
|
|
||||||
|
# XXX backport: ResourceWarning was added in Python 3.2.
|
||||||
|
# For earlier versions, fall back to RuntimeWarning instead.
|
||||||
|
_ResourceWarning = RuntimeWarning if sys.version_info < (3, 2) else ResourceWarning
|
||||||
|
|
||||||
|
|
||||||
|
class TemporaryDirectory(object):
|
||||||
|
"""Create and return a temporary directory. This has the same
|
||||||
|
behavior as mkdtemp but can be used as a context manager. For
|
||||||
|
example:
|
||||||
|
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
...
|
||||||
|
|
||||||
|
Upon exiting the context, the directory and everything contained
|
||||||
|
in it are removed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, suffix=None, prefix=None, dir=None):
|
||||||
|
self.name = mkdtemp(suffix, prefix, dir)
|
||||||
|
self._finalizer = finalize(
|
||||||
|
self, self._cleanup, self.name,
|
||||||
|
warn_message="Implicitly cleaning up {!r}".format(self))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _cleanup(cls, name, warn_message):
|
||||||
|
_rmtree(name)
|
||||||
|
_warnings.warn(warn_message, _ResourceWarning)
|
||||||
|
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<{} {!r}>".format(self.__class__.__name__, self.name)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __exit__(self, exc, value, tb):
|
||||||
|
self.cleanup()
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
if self._finalizer.detach():
|
||||||
|
_rmtree(self.name)
|
||||||
151
mixly/tools/python/backports/weakref.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
"""
|
||||||
|
Partial backport of Python 3.6's weakref module:
|
||||||
|
|
||||||
|
finalize (new in Python 3.4)
|
||||||
|
|
||||||
|
Backport modifications are marked with "XXX backport".
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
import sys
|
||||||
|
from weakref import ref
|
||||||
|
|
||||||
|
__all__ = ['finalize']
|
||||||
|
|
||||||
|
|
||||||
|
class finalize(object):
|
||||||
|
"""Class for finalization of weakrefable objects
|
||||||
|
|
||||||
|
finalize(obj, func, *args, **kwargs) returns a callable finalizer
|
||||||
|
object which will be called when obj is garbage collected. The
|
||||||
|
first time the finalizer is called it evaluates func(*arg, **kwargs)
|
||||||
|
and returns the result. After this the finalizer is dead, and
|
||||||
|
calling it just returns None.
|
||||||
|
|
||||||
|
When the program exits any remaining finalizers for which the
|
||||||
|
atexit attribute is true will be run in reverse order of creation.
|
||||||
|
By default atexit is true.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Finalizer objects don't have any state of their own. They are
|
||||||
|
# just used as keys to lookup _Info objects in the registry. This
|
||||||
|
# ensures that they cannot be part of a ref-cycle.
|
||||||
|
|
||||||
|
__slots__ = ()
|
||||||
|
_registry = {}
|
||||||
|
_shutdown = False
|
||||||
|
_index_iter = itertools.count()
|
||||||
|
_dirty = False
|
||||||
|
_registered_with_atexit = False
|
||||||
|
|
||||||
|
class _Info(object):
|
||||||
|
__slots__ = ("weakref", "func", "args", "kwargs", "atexit", "index")
|
||||||
|
|
||||||
|
def __init__(self, obj, func, *args, **kwargs):
|
||||||
|
if not self._registered_with_atexit:
|
||||||
|
# We may register the exit function more than once because
|
||||||
|
# of a thread race, but that is harmless
|
||||||
|
import atexit
|
||||||
|
atexit.register(self._exitfunc)
|
||||||
|
finalize._registered_with_atexit = True
|
||||||
|
info = self._Info()
|
||||||
|
info.weakref = ref(obj, self)
|
||||||
|
info.func = func
|
||||||
|
info.args = args
|
||||||
|
info.kwargs = kwargs or None
|
||||||
|
info.atexit = True
|
||||||
|
info.index = next(self._index_iter)
|
||||||
|
self._registry[self] = info
|
||||||
|
finalize._dirty = True
|
||||||
|
|
||||||
|
def __call__(self, _=None):
|
||||||
|
"""If alive then mark as dead and return func(*args, **kwargs);
|
||||||
|
otherwise return None"""
|
||||||
|
info = self._registry.pop(self, None)
|
||||||
|
if info and not self._shutdown:
|
||||||
|
return info.func(*info.args, **(info.kwargs or {}))
|
||||||
|
|
||||||
|
def detach(self):
|
||||||
|
"""If alive then mark as dead and return (obj, func, args, kwargs);
|
||||||
|
otherwise return None"""
|
||||||
|
info = self._registry.get(self)
|
||||||
|
obj = info and info.weakref()
|
||||||
|
if obj is not None and self._registry.pop(self, None):
|
||||||
|
return (obj, info.func, info.args, info.kwargs or {})
|
||||||
|
|
||||||
|
def peek(self):
|
||||||
|
"""If alive then return (obj, func, args, kwargs);
|
||||||
|
otherwise return None"""
|
||||||
|
info = self._registry.get(self)
|
||||||
|
obj = info and info.weakref()
|
||||||
|
if obj is not None:
|
||||||
|
return (obj, info.func, info.args, info.kwargs or {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def alive(self):
|
||||||
|
"""Whether finalizer is alive"""
|
||||||
|
return self in self._registry
|
||||||
|
|
||||||
|
@property
|
||||||
|
def atexit(self):
|
||||||
|
"""Whether finalizer should be called at exit"""
|
||||||
|
info = self._registry.get(self)
|
||||||
|
return bool(info) and info.atexit
|
||||||
|
|
||||||
|
@atexit.setter
|
||||||
|
def atexit(self, value):
|
||||||
|
info = self._registry.get(self)
|
||||||
|
if info:
|
||||||
|
info.atexit = bool(value)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
info = self._registry.get(self)
|
||||||
|
obj = info and info.weakref()
|
||||||
|
if obj is None:
|
||||||
|
return '<%s object at %#x; dead>' % (type(self).__name__, id(self))
|
||||||
|
else:
|
||||||
|
return '<%s object at %#x; for %r at %#x>' % \
|
||||||
|
(type(self).__name__, id(self), type(obj).__name__, id(obj))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _select_for_exit(cls):
|
||||||
|
# Return live finalizers marked for exit, oldest first
|
||||||
|
L = [(f,i) for (f,i) in cls._registry.items() if i.atexit]
|
||||||
|
L.sort(key=lambda item:item[1].index)
|
||||||
|
return [f for (f,i) in L]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _exitfunc(cls):
|
||||||
|
# At shutdown invoke finalizers for which atexit is true.
|
||||||
|
# This is called once all other non-daemonic threads have been
|
||||||
|
# joined.
|
||||||
|
reenable_gc = False
|
||||||
|
try:
|
||||||
|
if cls._registry:
|
||||||
|
import gc
|
||||||
|
if gc.isenabled():
|
||||||
|
reenable_gc = True
|
||||||
|
gc.disable()
|
||||||
|
pending = None
|
||||||
|
while True:
|
||||||
|
if pending is None or finalize._dirty:
|
||||||
|
pending = cls._select_for_exit()
|
||||||
|
finalize._dirty = False
|
||||||
|
if not pending:
|
||||||
|
break
|
||||||
|
f = pending.pop()
|
||||||
|
try:
|
||||||
|
# gc is disabled, so (assuming no daemonic
|
||||||
|
# threads) the following is the only line in
|
||||||
|
# this function which might trigger creation
|
||||||
|
# of a new finalizer
|
||||||
|
f()
|
||||||
|
except Exception:
|
||||||
|
sys.excepthook(*sys.exc_info())
|
||||||
|
assert f not in cls._registry
|
||||||
|
finally:
|
||||||
|
# prevent any more finalizers from executing during shutdown
|
||||||
|
finalize._shutdown = True
|
||||||
|
if reenable_gc:
|
||||||
|
gc.enable()
|
||||||
79
mixly/tools/python/click/__init__.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
Click is a simple Python module inspired by the stdlib optparse to make
|
||||||
|
writing command line scripts fun. Unlike other modules, it's based
|
||||||
|
around a simple API that does not come with too much magic and is
|
||||||
|
composable.
|
||||||
|
"""
|
||||||
|
from .core import Argument
|
||||||
|
from .core import BaseCommand
|
||||||
|
from .core import Command
|
||||||
|
from .core import CommandCollection
|
||||||
|
from .core import Context
|
||||||
|
from .core import Group
|
||||||
|
from .core import MultiCommand
|
||||||
|
from .core import Option
|
||||||
|
from .core import Parameter
|
||||||
|
from .decorators import argument
|
||||||
|
from .decorators import command
|
||||||
|
from .decorators import confirmation_option
|
||||||
|
from .decorators import group
|
||||||
|
from .decorators import help_option
|
||||||
|
from .decorators import make_pass_decorator
|
||||||
|
from .decorators import option
|
||||||
|
from .decorators import pass_context
|
||||||
|
from .decorators import pass_obj
|
||||||
|
from .decorators import password_option
|
||||||
|
from .decorators import version_option
|
||||||
|
from .exceptions import Abort
|
||||||
|
from .exceptions import BadArgumentUsage
|
||||||
|
from .exceptions import BadOptionUsage
|
||||||
|
from .exceptions import BadParameter
|
||||||
|
from .exceptions import ClickException
|
||||||
|
from .exceptions import FileError
|
||||||
|
from .exceptions import MissingParameter
|
||||||
|
from .exceptions import NoSuchOption
|
||||||
|
from .exceptions import UsageError
|
||||||
|
from .formatting import HelpFormatter
|
||||||
|
from .formatting import wrap_text
|
||||||
|
from .globals import get_current_context
|
||||||
|
from .parser import OptionParser
|
||||||
|
from .termui import clear
|
||||||
|
from .termui import confirm
|
||||||
|
from .termui import echo_via_pager
|
||||||
|
from .termui import edit
|
||||||
|
from .termui import get_terminal_size
|
||||||
|
from .termui import getchar
|
||||||
|
from .termui import launch
|
||||||
|
from .termui import pause
|
||||||
|
from .termui import progressbar
|
||||||
|
from .termui import prompt
|
||||||
|
from .termui import secho
|
||||||
|
from .termui import style
|
||||||
|
from .termui import unstyle
|
||||||
|
from .types import BOOL
|
||||||
|
from .types import Choice
|
||||||
|
from .types import DateTime
|
||||||
|
from .types import File
|
||||||
|
from .types import FLOAT
|
||||||
|
from .types import FloatRange
|
||||||
|
from .types import INT
|
||||||
|
from .types import IntRange
|
||||||
|
from .types import ParamType
|
||||||
|
from .types import Path
|
||||||
|
from .types import STRING
|
||||||
|
from .types import Tuple
|
||||||
|
from .types import UNPROCESSED
|
||||||
|
from .types import UUID
|
||||||
|
from .utils import echo
|
||||||
|
from .utils import format_filename
|
||||||
|
from .utils import get_app_dir
|
||||||
|
from .utils import get_binary_stream
|
||||||
|
from .utils import get_os_args
|
||||||
|
from .utils import get_text_stream
|
||||||
|
from .utils import open_file
|
||||||
|
|
||||||
|
# Controls if click should emit the warning about the use of unicode
|
||||||
|
# literals.
|
||||||
|
disable_unicode_literals_warning = False
|
||||||
|
|
||||||
|
__version__ = "7.1.2"
|
||||||
375
mixly/tools/python/click/_bashcomplete.py
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
import copy
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .core import Argument
|
||||||
|
from .core import MultiCommand
|
||||||
|
from .core import Option
|
||||||
|
from .parser import split_arg_string
|
||||||
|
from .types import Choice
|
||||||
|
from .utils import echo
|
||||||
|
|
||||||
|
try:
|
||||||
|
from collections import abc
|
||||||
|
except ImportError:
|
||||||
|
import collections as abc
|
||||||
|
|
||||||
|
WORDBREAK = "="
|
||||||
|
|
||||||
|
# Note, only BASH version 4.4 and later have the nosort option.
|
||||||
|
COMPLETION_SCRIPT_BASH = """
|
||||||
|
%(complete_func)s() {
|
||||||
|
local IFS=$'\n'
|
||||||
|
COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\
|
||||||
|
COMP_CWORD=$COMP_CWORD \\
|
||||||
|
%(autocomplete_var)s=complete $1 ) )
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
%(complete_func)setup() {
|
||||||
|
local COMPLETION_OPTIONS=""
|
||||||
|
local BASH_VERSION_ARR=(${BASH_VERSION//./ })
|
||||||
|
# Only BASH version 4.4 and later have the nosort option.
|
||||||
|
if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] \
|
||||||
|
&& [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then
|
||||||
|
COMPLETION_OPTIONS="-o nosort"
|
||||||
|
fi
|
||||||
|
|
||||||
|
complete $COMPLETION_OPTIONS -F %(complete_func)s %(script_names)s
|
||||||
|
}
|
||||||
|
|
||||||
|
%(complete_func)setup
|
||||||
|
"""
|
||||||
|
|
||||||
|
COMPLETION_SCRIPT_ZSH = """
|
||||||
|
#compdef %(script_names)s
|
||||||
|
|
||||||
|
%(complete_func)s() {
|
||||||
|
local -a completions
|
||||||
|
local -a completions_with_descriptions
|
||||||
|
local -a response
|
||||||
|
(( ! $+commands[%(script_names)s] )) && return 1
|
||||||
|
|
||||||
|
response=("${(@f)$( env COMP_WORDS=\"${words[*]}\" \\
|
||||||
|
COMP_CWORD=$((CURRENT-1)) \\
|
||||||
|
%(autocomplete_var)s=\"complete_zsh\" \\
|
||||||
|
%(script_names)s )}")
|
||||||
|
|
||||||
|
for key descr in ${(kv)response}; do
|
||||||
|
if [[ "$descr" == "_" ]]; then
|
||||||
|
completions+=("$key")
|
||||||
|
else
|
||||||
|
completions_with_descriptions+=("$key":"$descr")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$completions_with_descriptions" ]; then
|
||||||
|
_describe -V unsorted completions_with_descriptions -U
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$completions" ]; then
|
||||||
|
compadd -U -V unsorted -a completions
|
||||||
|
fi
|
||||||
|
compstate[insert]="automenu"
|
||||||
|
}
|
||||||
|
|
||||||
|
compdef %(complete_func)s %(script_names)s
|
||||||
|
"""
|
||||||
|
|
||||||
|
COMPLETION_SCRIPT_FISH = (
|
||||||
|
"complete --no-files --command %(script_names)s --arguments"
|
||||||
|
' "(env %(autocomplete_var)s=complete_fish'
|
||||||
|
" COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t)"
|
||||||
|
' %(script_names)s)"'
|
||||||
|
)
|
||||||
|
|
||||||
|
_completion_scripts = {
|
||||||
|
"bash": COMPLETION_SCRIPT_BASH,
|
||||||
|
"zsh": COMPLETION_SCRIPT_ZSH,
|
||||||
|
"fish": COMPLETION_SCRIPT_FISH,
|
||||||
|
}
|
||||||
|
|
||||||
|
_invalid_ident_char_re = re.compile(r"[^a-zA-Z0-9_]")
|
||||||
|
|
||||||
|
|
||||||
|
def get_completion_script(prog_name, complete_var, shell):
|
||||||
|
cf_name = _invalid_ident_char_re.sub("", prog_name.replace("-", "_"))
|
||||||
|
script = _completion_scripts.get(shell, COMPLETION_SCRIPT_BASH)
|
||||||
|
return (
|
||||||
|
script
|
||||||
|
% {
|
||||||
|
"complete_func": "_{}_completion".format(cf_name),
|
||||||
|
"script_names": prog_name,
|
||||||
|
"autocomplete_var": complete_var,
|
||||||
|
}
|
||||||
|
).strip() + ";"
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_ctx(cli, prog_name, args):
|
||||||
|
"""Parse into a hierarchy of contexts. Contexts are connected
|
||||||
|
through the parent variable.
|
||||||
|
|
||||||
|
:param cli: command definition
|
||||||
|
:param prog_name: the program that is running
|
||||||
|
:param args: full list of args
|
||||||
|
:return: the final context/command parsed
|
||||||
|
"""
|
||||||
|
ctx = cli.make_context(prog_name, args, resilient_parsing=True)
|
||||||
|
args = ctx.protected_args + ctx.args
|
||||||
|
while args:
|
||||||
|
if isinstance(ctx.command, MultiCommand):
|
||||||
|
if not ctx.command.chain:
|
||||||
|
cmd_name, cmd, args = ctx.command.resolve_command(ctx, args)
|
||||||
|
if cmd is None:
|
||||||
|
return ctx
|
||||||
|
ctx = cmd.make_context(
|
||||||
|
cmd_name, args, parent=ctx, resilient_parsing=True
|
||||||
|
)
|
||||||
|
args = ctx.protected_args + ctx.args
|
||||||
|
else:
|
||||||
|
# Walk chained subcommand contexts saving the last one.
|
||||||
|
while args:
|
||||||
|
cmd_name, cmd, args = ctx.command.resolve_command(ctx, args)
|
||||||
|
if cmd is None:
|
||||||
|
return ctx
|
||||||
|
sub_ctx = cmd.make_context(
|
||||||
|
cmd_name,
|
||||||
|
args,
|
||||||
|
parent=ctx,
|
||||||
|
allow_extra_args=True,
|
||||||
|
allow_interspersed_args=False,
|
||||||
|
resilient_parsing=True,
|
||||||
|
)
|
||||||
|
args = sub_ctx.args
|
||||||
|
ctx = sub_ctx
|
||||||
|
args = sub_ctx.protected_args + sub_ctx.args
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
def start_of_option(param_str):
|
||||||
|
"""
|
||||||
|
:param param_str: param_str to check
|
||||||
|
:return: whether or not this is the start of an option declaration
|
||||||
|
(i.e. starts "-" or "--")
|
||||||
|
"""
|
||||||
|
return param_str and param_str[:1] == "-"
|
||||||
|
|
||||||
|
|
||||||
|
def is_incomplete_option(all_args, cmd_param):
|
||||||
|
"""
|
||||||
|
:param all_args: the full original list of args supplied
|
||||||
|
:param cmd_param: the current command paramter
|
||||||
|
:return: whether or not the last option declaration (i.e. starts
|
||||||
|
"-" or "--") is incomplete and corresponds to this cmd_param. In
|
||||||
|
other words whether this cmd_param option can still accept
|
||||||
|
values
|
||||||
|
"""
|
||||||
|
if not isinstance(cmd_param, Option):
|
||||||
|
return False
|
||||||
|
if cmd_param.is_flag:
|
||||||
|
return False
|
||||||
|
last_option = None
|
||||||
|
for index, arg_str in enumerate(
|
||||||
|
reversed([arg for arg in all_args if arg != WORDBREAK])
|
||||||
|
):
|
||||||
|
if index + 1 > cmd_param.nargs:
|
||||||
|
break
|
||||||
|
if start_of_option(arg_str):
|
||||||
|
last_option = arg_str
|
||||||
|
|
||||||
|
return True if last_option and last_option in cmd_param.opts else False
|
||||||
|
|
||||||
|
|
||||||
|
def is_incomplete_argument(current_params, cmd_param):
|
||||||
|
"""
|
||||||
|
:param current_params: the current params and values for this
|
||||||
|
argument as already entered
|
||||||
|
:param cmd_param: the current command parameter
|
||||||
|
:return: whether or not the last argument is incomplete and
|
||||||
|
corresponds to this cmd_param. In other words whether or not the
|
||||||
|
this cmd_param argument can still accept values
|
||||||
|
"""
|
||||||
|
if not isinstance(cmd_param, Argument):
|
||||||
|
return False
|
||||||
|
current_param_values = current_params[cmd_param.name]
|
||||||
|
if current_param_values is None:
|
||||||
|
return True
|
||||||
|
if cmd_param.nargs == -1:
|
||||||
|
return True
|
||||||
|
if (
|
||||||
|
isinstance(current_param_values, abc.Iterable)
|
||||||
|
and cmd_param.nargs > 1
|
||||||
|
and len(current_param_values) < cmd_param.nargs
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_autocompletions(ctx, args, incomplete, cmd_param):
|
||||||
|
"""
|
||||||
|
:param ctx: context associated with the parsed command
|
||||||
|
:param args: full list of args
|
||||||
|
:param incomplete: the incomplete text to autocomplete
|
||||||
|
:param cmd_param: command definition
|
||||||
|
:return: all the possible user-specified completions for the param
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
if isinstance(cmd_param.type, Choice):
|
||||||
|
# Choices don't support descriptions.
|
||||||
|
results = [
|
||||||
|
(c, None) for c in cmd_param.type.choices if str(c).startswith(incomplete)
|
||||||
|
]
|
||||||
|
elif cmd_param.autocompletion is not None:
|
||||||
|
dynamic_completions = cmd_param.autocompletion(
|
||||||
|
ctx=ctx, args=args, incomplete=incomplete
|
||||||
|
)
|
||||||
|
results = [
|
||||||
|
c if isinstance(c, tuple) else (c, None) for c in dynamic_completions
|
||||||
|
]
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def get_visible_commands_starting_with(ctx, starts_with):
|
||||||
|
"""
|
||||||
|
:param ctx: context associated with the parsed command
|
||||||
|
:starts_with: string that visible commands must start with.
|
||||||
|
:return: all visible (not hidden) commands that start with starts_with.
|
||||||
|
"""
|
||||||
|
for c in ctx.command.list_commands(ctx):
|
||||||
|
if c.startswith(starts_with):
|
||||||
|
command = ctx.command.get_command(ctx, c)
|
||||||
|
if not command.hidden:
|
||||||
|
yield command
|
||||||
|
|
||||||
|
|
||||||
|
def add_subcommand_completions(ctx, incomplete, completions_out):
|
||||||
|
# Add subcommand completions.
|
||||||
|
if isinstance(ctx.command, MultiCommand):
|
||||||
|
completions_out.extend(
|
||||||
|
[
|
||||||
|
(c.name, c.get_short_help_str())
|
||||||
|
for c in get_visible_commands_starting_with(ctx, incomplete)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Walk up the context list and add any other completion
|
||||||
|
# possibilities from chained commands
|
||||||
|
while ctx.parent is not None:
|
||||||
|
ctx = ctx.parent
|
||||||
|
if isinstance(ctx.command, MultiCommand) and ctx.command.chain:
|
||||||
|
remaining_commands = [
|
||||||
|
c
|
||||||
|
for c in get_visible_commands_starting_with(ctx, incomplete)
|
||||||
|
if c.name not in ctx.protected_args
|
||||||
|
]
|
||||||
|
completions_out.extend(
|
||||||
|
[(c.name, c.get_short_help_str()) for c in remaining_commands]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_choices(cli, prog_name, args, incomplete):
|
||||||
|
"""
|
||||||
|
:param cli: command definition
|
||||||
|
:param prog_name: the program that is running
|
||||||
|
:param args: full list of args
|
||||||
|
:param incomplete: the incomplete text to autocomplete
|
||||||
|
:return: all the possible completions for the incomplete
|
||||||
|
"""
|
||||||
|
all_args = copy.deepcopy(args)
|
||||||
|
|
||||||
|
ctx = resolve_ctx(cli, prog_name, args)
|
||||||
|
if ctx is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
has_double_dash = "--" in all_args
|
||||||
|
|
||||||
|
# In newer versions of bash long opts with '='s are partitioned, but
|
||||||
|
# it's easier to parse without the '='
|
||||||
|
if start_of_option(incomplete) and WORDBREAK in incomplete:
|
||||||
|
partition_incomplete = incomplete.partition(WORDBREAK)
|
||||||
|
all_args.append(partition_incomplete[0])
|
||||||
|
incomplete = partition_incomplete[2]
|
||||||
|
elif incomplete == WORDBREAK:
|
||||||
|
incomplete = ""
|
||||||
|
|
||||||
|
completions = []
|
||||||
|
if not has_double_dash and start_of_option(incomplete):
|
||||||
|
# completions for partial options
|
||||||
|
for param in ctx.command.params:
|
||||||
|
if isinstance(param, Option) and not param.hidden:
|
||||||
|
param_opts = [
|
||||||
|
param_opt
|
||||||
|
for param_opt in param.opts + param.secondary_opts
|
||||||
|
if param_opt not in all_args or param.multiple
|
||||||
|
]
|
||||||
|
completions.extend(
|
||||||
|
[(o, param.help) for o in param_opts if o.startswith(incomplete)]
|
||||||
|
)
|
||||||
|
return completions
|
||||||
|
# completion for option values from user supplied values
|
||||||
|
for param in ctx.command.params:
|
||||||
|
if is_incomplete_option(all_args, param):
|
||||||
|
return get_user_autocompletions(ctx, all_args, incomplete, param)
|
||||||
|
# completion for argument values from user supplied values
|
||||||
|
for param in ctx.command.params:
|
||||||
|
if is_incomplete_argument(ctx.params, param):
|
||||||
|
return get_user_autocompletions(ctx, all_args, incomplete, param)
|
||||||
|
|
||||||
|
add_subcommand_completions(ctx, incomplete, completions)
|
||||||
|
# Sort before returning so that proper ordering can be enforced in custom types.
|
||||||
|
return sorted(completions)
|
||||||
|
|
||||||
|
|
||||||
|
def do_complete(cli, prog_name, include_descriptions):
|
||||||
|
cwords = split_arg_string(os.environ["COMP_WORDS"])
|
||||||
|
cword = int(os.environ["COMP_CWORD"])
|
||||||
|
args = cwords[1:cword]
|
||||||
|
try:
|
||||||
|
incomplete = cwords[cword]
|
||||||
|
except IndexError:
|
||||||
|
incomplete = ""
|
||||||
|
|
||||||
|
for item in get_choices(cli, prog_name, args, incomplete):
|
||||||
|
echo(item[0])
|
||||||
|
if include_descriptions:
|
||||||
|
# ZSH has trouble dealing with empty array parameters when
|
||||||
|
# returned from commands, use '_' to indicate no description
|
||||||
|
# is present.
|
||||||
|
echo(item[1] if item[1] else "_")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def do_complete_fish(cli, prog_name):
|
||||||
|
cwords = split_arg_string(os.environ["COMP_WORDS"])
|
||||||
|
incomplete = os.environ["COMP_CWORD"]
|
||||||
|
args = cwords[1:]
|
||||||
|
|
||||||
|
for item in get_choices(cli, prog_name, args, incomplete):
|
||||||
|
if item[1]:
|
||||||
|
echo("{arg}\t{desc}".format(arg=item[0], desc=item[1]))
|
||||||
|
else:
|
||||||
|
echo(item[0])
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def bashcomplete(cli, prog_name, complete_var, complete_instr):
|
||||||
|
if "_" in complete_instr:
|
||||||
|
command, shell = complete_instr.split("_", 1)
|
||||||
|
else:
|
||||||
|
command = complete_instr
|
||||||
|
shell = "bash"
|
||||||
|
|
||||||
|
if command == "source":
|
||||||
|
echo(get_completion_script(prog_name, complete_var, shell))
|
||||||
|
return True
|
||||||
|
elif command == "complete":
|
||||||
|
if shell == "fish":
|
||||||
|
return do_complete_fish(cli, prog_name)
|
||||||
|
elif shell in {"bash", "zsh"}:
|
||||||
|
return do_complete(cli, prog_name, shell == "zsh")
|
||||||
|
|
||||||
|
return False
|
||||||
786
mixly/tools/python/click/_compat.py
Normal file
@@ -0,0 +1,786 @@
|
|||||||
|
# flake8: noqa
|
||||||
|
import codecs
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from weakref import WeakKeyDictionary
|
||||||
|
|
||||||
|
PY2 = sys.version_info[0] == 2
|
||||||
|
CYGWIN = sys.platform.startswith("cygwin")
|
||||||
|
MSYS2 = sys.platform.startswith("win") and ("GCC" in sys.version)
|
||||||
|
# Determine local App Engine environment, per Google's own suggestion
|
||||||
|
APP_ENGINE = "APPENGINE_RUNTIME" in os.environ and "Development/" in os.environ.get(
|
||||||
|
"SERVER_SOFTWARE", ""
|
||||||
|
)
|
||||||
|
WIN = sys.platform.startswith("win") and not APP_ENGINE and not MSYS2
|
||||||
|
DEFAULT_COLUMNS = 80
|
||||||
|
|
||||||
|
|
||||||
|
_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]")
|
||||||
|
|
||||||
|
|
||||||
|
def get_filesystem_encoding():
|
||||||
|
return sys.getfilesystemencoding() or sys.getdefaultencoding()
|
||||||
|
|
||||||
|
|
||||||
|
def _make_text_stream(
|
||||||
|
stream, encoding, errors, force_readable=False, force_writable=False
|
||||||
|
):
|
||||||
|
if encoding is None:
|
||||||
|
encoding = get_best_encoding(stream)
|
||||||
|
if errors is None:
|
||||||
|
errors = "replace"
|
||||||
|
return _NonClosingTextIOWrapper(
|
||||||
|
stream,
|
||||||
|
encoding,
|
||||||
|
errors,
|
||||||
|
line_buffering=True,
|
||||||
|
force_readable=force_readable,
|
||||||
|
force_writable=force_writable,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_ascii_encoding(encoding):
|
||||||
|
"""Checks if a given encoding is ascii."""
|
||||||
|
try:
|
||||||
|
return codecs.lookup(encoding).name == "ascii"
|
||||||
|
except LookupError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_best_encoding(stream):
|
||||||
|
"""Returns the default stream encoding if not found."""
|
||||||
|
rv = getattr(stream, "encoding", None) or sys.getdefaultencoding()
|
||||||
|
if is_ascii_encoding(rv):
|
||||||
|
return "utf-8"
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
class _NonClosingTextIOWrapper(io.TextIOWrapper):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
stream,
|
||||||
|
encoding,
|
||||||
|
errors,
|
||||||
|
force_readable=False,
|
||||||
|
force_writable=False,
|
||||||
|
**extra
|
||||||
|
):
|
||||||
|
self._stream = stream = _FixupStream(stream, force_readable, force_writable)
|
||||||
|
io.TextIOWrapper.__init__(self, stream, encoding, errors, **extra)
|
||||||
|
|
||||||
|
# The io module is a place where the Python 3 text behavior
|
||||||
|
# was forced upon Python 2, so we need to unbreak
|
||||||
|
# it to look like Python 2.
|
||||||
|
if PY2:
|
||||||
|
|
||||||
|
def write(self, x):
|
||||||
|
if isinstance(x, str) or is_bytes(x):
|
||||||
|
try:
|
||||||
|
self.flush()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return self.buffer.write(str(x))
|
||||||
|
return io.TextIOWrapper.write(self, x)
|
||||||
|
|
||||||
|
def writelines(self, lines):
|
||||||
|
for line in lines:
|
||||||
|
self.write(line)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
try:
|
||||||
|
self.detach()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def isatty(self):
|
||||||
|
# https://bitbucket.org/pypy/pypy/issue/1803
|
||||||
|
return self._stream.isatty()
|
||||||
|
|
||||||
|
|
||||||
|
class _FixupStream(object):
|
||||||
|
"""The new io interface needs more from streams than streams
|
||||||
|
traditionally implement. As such, this fix-up code is necessary in
|
||||||
|
some circumstances.
|
||||||
|
|
||||||
|
The forcing of readable and writable flags are there because some tools
|
||||||
|
put badly patched objects on sys (one such offender are certain version
|
||||||
|
of jupyter notebook).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, stream, force_readable=False, force_writable=False):
|
||||||
|
self._stream = stream
|
||||||
|
self._force_readable = force_readable
|
||||||
|
self._force_writable = force_writable
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self._stream, name)
|
||||||
|
|
||||||
|
def read1(self, size):
|
||||||
|
f = getattr(self._stream, "read1", None)
|
||||||
|
if f is not None:
|
||||||
|
return f(size)
|
||||||
|
# We only dispatch to readline instead of read in Python 2 as we
|
||||||
|
# do not want cause problems with the different implementation
|
||||||
|
# of line buffering.
|
||||||
|
if PY2:
|
||||||
|
return self._stream.readline(size)
|
||||||
|
return self._stream.read(size)
|
||||||
|
|
||||||
|
def readable(self):
|
||||||
|
if self._force_readable:
|
||||||
|
return True
|
||||||
|
x = getattr(self._stream, "readable", None)
|
||||||
|
if x is not None:
|
||||||
|
return x()
|
||||||
|
try:
|
||||||
|
self._stream.read(0)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def writable(self):
|
||||||
|
if self._force_writable:
|
||||||
|
return True
|
||||||
|
x = getattr(self._stream, "writable", None)
|
||||||
|
if x is not None:
|
||||||
|
return x()
|
||||||
|
try:
|
||||||
|
self._stream.write("")
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self._stream.write(b"")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def seekable(self):
|
||||||
|
x = getattr(self._stream, "seekable", None)
|
||||||
|
if x is not None:
|
||||||
|
return x()
|
||||||
|
try:
|
||||||
|
self._stream.seek(self._stream.tell())
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
if PY2:
|
||||||
|
text_type = unicode
|
||||||
|
raw_input = raw_input
|
||||||
|
string_types = (str, unicode)
|
||||||
|
int_types = (int, long)
|
||||||
|
iteritems = lambda x: x.iteritems()
|
||||||
|
range_type = xrange
|
||||||
|
|
||||||
|
def is_bytes(x):
|
||||||
|
return isinstance(x, (buffer, bytearray))
|
||||||
|
|
||||||
|
_identifier_re = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
|
||||||
|
|
||||||
|
# For Windows, we need to force stdout/stdin/stderr to binary if it's
|
||||||
|
# fetched for that. This obviously is not the most correct way to do
|
||||||
|
# it as it changes global state. Unfortunately, there does not seem to
|
||||||
|
# be a clear better way to do it as just reopening the file in binary
|
||||||
|
# mode does not change anything.
|
||||||
|
#
|
||||||
|
# An option would be to do what Python 3 does and to open the file as
|
||||||
|
# binary only, patch it back to the system, and then use a wrapper
|
||||||
|
# stream that converts newlines. It's not quite clear what's the
|
||||||
|
# correct option here.
|
||||||
|
#
|
||||||
|
# This code also lives in _winconsole for the fallback to the console
|
||||||
|
# emulation stream.
|
||||||
|
#
|
||||||
|
# There are also Windows environments where the `msvcrt` module is not
|
||||||
|
# available (which is why we use try-catch instead of the WIN variable
|
||||||
|
# here), such as the Google App Engine development server on Windows. In
|
||||||
|
# those cases there is just nothing we can do.
|
||||||
|
def set_binary_mode(f):
|
||||||
|
return f
|
||||||
|
|
||||||
|
try:
|
||||||
|
import msvcrt
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
|
||||||
|
def set_binary_mode(f):
|
||||||
|
try:
|
||||||
|
fileno = f.fileno()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
msvcrt.setmode(fileno, os.O_BINARY)
|
||||||
|
return f
|
||||||
|
|
||||||
|
try:
|
||||||
|
import fcntl
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
|
||||||
|
def set_binary_mode(f):
|
||||||
|
try:
|
||||||
|
fileno = f.fileno()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
flags = fcntl.fcntl(fileno, fcntl.F_GETFL)
|
||||||
|
fcntl.fcntl(fileno, fcntl.F_SETFL, flags & ~os.O_NONBLOCK)
|
||||||
|
return f
|
||||||
|
|
||||||
|
def isidentifier(x):
|
||||||
|
return _identifier_re.search(x) is not None
|
||||||
|
|
||||||
|
def get_binary_stdin():
|
||||||
|
return set_binary_mode(sys.stdin)
|
||||||
|
|
||||||
|
def get_binary_stdout():
|
||||||
|
_wrap_std_stream("stdout")
|
||||||
|
return set_binary_mode(sys.stdout)
|
||||||
|
|
||||||
|
def get_binary_stderr():
|
||||||
|
_wrap_std_stream("stderr")
|
||||||
|
return set_binary_mode(sys.stderr)
|
||||||
|
|
||||||
|
def get_text_stdin(encoding=None, errors=None):
|
||||||
|
rv = _get_windows_console_stream(sys.stdin, encoding, errors)
|
||||||
|
if rv is not None:
|
||||||
|
return rv
|
||||||
|
return _make_text_stream(sys.stdin, encoding, errors, force_readable=True)
|
||||||
|
|
||||||
|
def get_text_stdout(encoding=None, errors=None):
|
||||||
|
_wrap_std_stream("stdout")
|
||||||
|
rv = _get_windows_console_stream(sys.stdout, encoding, errors)
|
||||||
|
if rv is not None:
|
||||||
|
return rv
|
||||||
|
return _make_text_stream(sys.stdout, encoding, errors, force_writable=True)
|
||||||
|
|
||||||
|
def get_text_stderr(encoding=None, errors=None):
|
||||||
|
_wrap_std_stream("stderr")
|
||||||
|
rv = _get_windows_console_stream(sys.stderr, encoding, errors)
|
||||||
|
if rv is not None:
|
||||||
|
return rv
|
||||||
|
return _make_text_stream(sys.stderr, encoding, errors, force_writable=True)
|
||||||
|
|
||||||
|
def filename_to_ui(value):
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
value = value.decode(get_filesystem_encoding(), "replace")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
import io
|
||||||
|
|
||||||
|
text_type = str
|
||||||
|
raw_input = input
|
||||||
|
string_types = (str,)
|
||||||
|
int_types = (int,)
|
||||||
|
range_type = range
|
||||||
|
isidentifier = lambda x: x.isidentifier()
|
||||||
|
iteritems = lambda x: iter(x.items())
|
||||||
|
|
||||||
|
def is_bytes(x):
|
||||||
|
return isinstance(x, (bytes, memoryview, bytearray))
|
||||||
|
|
||||||
|
def _is_binary_reader(stream, default=False):
|
||||||
|
try:
|
||||||
|
return isinstance(stream.read(0), bytes)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
# This happens in some cases where the stream was already
|
||||||
|
# closed. In this case, we assume the default.
|
||||||
|
|
||||||
|
def _is_binary_writer(stream, default=False):
|
||||||
|
try:
|
||||||
|
stream.write(b"")
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
stream.write("")
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return default
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _find_binary_reader(stream):
|
||||||
|
# We need to figure out if the given stream is already binary.
|
||||||
|
# This can happen because the official docs recommend detaching
|
||||||
|
# the streams to get binary streams. Some code might do this, so
|
||||||
|
# we need to deal with this case explicitly.
|
||||||
|
if _is_binary_reader(stream, False):
|
||||||
|
return stream
|
||||||
|
|
||||||
|
buf = getattr(stream, "buffer", None)
|
||||||
|
|
||||||
|
# Same situation here; this time we assume that the buffer is
|
||||||
|
# actually binary in case it's closed.
|
||||||
|
if buf is not None and _is_binary_reader(buf, True):
|
||||||
|
return buf
|
||||||
|
|
||||||
|
def _find_binary_writer(stream):
|
||||||
|
# We need to figure out if the given stream is already binary.
|
||||||
|
# This can happen because the official docs recommend detatching
|
||||||
|
# the streams to get binary streams. Some code might do this, so
|
||||||
|
# we need to deal with this case explicitly.
|
||||||
|
if _is_binary_writer(stream, False):
|
||||||
|
return stream
|
||||||
|
|
||||||
|
buf = getattr(stream, "buffer", None)
|
||||||
|
|
||||||
|
# Same situation here; this time we assume that the buffer is
|
||||||
|
# actually binary in case it's closed.
|
||||||
|
if buf is not None and _is_binary_writer(buf, True):
|
||||||
|
return buf
|
||||||
|
|
||||||
|
def _stream_is_misconfigured(stream):
|
||||||
|
"""A stream is misconfigured if its encoding is ASCII."""
|
||||||
|
# If the stream does not have an encoding set, we assume it's set
|
||||||
|
# to ASCII. This appears to happen in certain unittest
|
||||||
|
# environments. It's not quite clear what the correct behavior is
|
||||||
|
# but this at least will force Click to recover somehow.
|
||||||
|
return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii")
|
||||||
|
|
||||||
|
def _is_compat_stream_attr(stream, attr, value):
|
||||||
|
"""A stream attribute is compatible if it is equal to the
|
||||||
|
desired value or the desired value is unset and the attribute
|
||||||
|
has a value.
|
||||||
|
"""
|
||||||
|
stream_value = getattr(stream, attr, None)
|
||||||
|
return stream_value == value or (value is None and stream_value is not None)
|
||||||
|
|
||||||
|
def _is_compatible_text_stream(stream, encoding, errors):
|
||||||
|
"""Check if a stream's encoding and errors attributes are
|
||||||
|
compatible with the desired values.
|
||||||
|
"""
|
||||||
|
return _is_compat_stream_attr(
|
||||||
|
stream, "encoding", encoding
|
||||||
|
) and _is_compat_stream_attr(stream, "errors", errors)
|
||||||
|
|
||||||
|
def _force_correct_text_stream(
|
||||||
|
text_stream,
|
||||||
|
encoding,
|
||||||
|
errors,
|
||||||
|
is_binary,
|
||||||
|
find_binary,
|
||||||
|
force_readable=False,
|
||||||
|
force_writable=False,
|
||||||
|
):
|
||||||
|
if is_binary(text_stream, False):
|
||||||
|
binary_reader = text_stream
|
||||||
|
else:
|
||||||
|
# If the stream looks compatible, and won't default to a
|
||||||
|
# misconfigured ascii encoding, return it as-is.
|
||||||
|
if _is_compatible_text_stream(text_stream, encoding, errors) and not (
|
||||||
|
encoding is None and _stream_is_misconfigured(text_stream)
|
||||||
|
):
|
||||||
|
return text_stream
|
||||||
|
|
||||||
|
# Otherwise, get the underlying binary reader.
|
||||||
|
binary_reader = find_binary(text_stream)
|
||||||
|
|
||||||
|
# If that's not possible, silently use the original reader
|
||||||
|
# and get mojibake instead of exceptions.
|
||||||
|
if binary_reader is None:
|
||||||
|
return text_stream
|
||||||
|
|
||||||
|
# Default errors to replace instead of strict in order to get
|
||||||
|
# something that works.
|
||||||
|
if errors is None:
|
||||||
|
errors = "replace"
|
||||||
|
|
||||||
|
# Wrap the binary stream in a text stream with the correct
|
||||||
|
# encoding parameters.
|
||||||
|
return _make_text_stream(
|
||||||
|
binary_reader,
|
||||||
|
encoding,
|
||||||
|
errors,
|
||||||
|
force_readable=force_readable,
|
||||||
|
force_writable=force_writable,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _force_correct_text_reader(text_reader, encoding, errors, force_readable=False):
|
||||||
|
return _force_correct_text_stream(
|
||||||
|
text_reader,
|
||||||
|
encoding,
|
||||||
|
errors,
|
||||||
|
_is_binary_reader,
|
||||||
|
_find_binary_reader,
|
||||||
|
force_readable=force_readable,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _force_correct_text_writer(text_writer, encoding, errors, force_writable=False):
|
||||||
|
return _force_correct_text_stream(
|
||||||
|
text_writer,
|
||||||
|
encoding,
|
||||||
|
errors,
|
||||||
|
_is_binary_writer,
|
||||||
|
_find_binary_writer,
|
||||||
|
force_writable=force_writable,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_binary_stdin():
|
||||||
|
reader = _find_binary_reader(sys.stdin)
|
||||||
|
if reader is None:
|
||||||
|
raise RuntimeError("Was not able to determine binary stream for sys.stdin.")
|
||||||
|
return reader
|
||||||
|
|
||||||
|
def get_binary_stdout():
|
||||||
|
writer = _find_binary_writer(sys.stdout)
|
||||||
|
if writer is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Was not able to determine binary stream for sys.stdout."
|
||||||
|
)
|
||||||
|
return writer
|
||||||
|
|
||||||
|
def get_binary_stderr():
|
||||||
|
writer = _find_binary_writer(sys.stderr)
|
||||||
|
if writer is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Was not able to determine binary stream for sys.stderr."
|
||||||
|
)
|
||||||
|
return writer
|
||||||
|
|
||||||
|
def get_text_stdin(encoding=None, errors=None):
|
||||||
|
rv = _get_windows_console_stream(sys.stdin, encoding, errors)
|
||||||
|
if rv is not None:
|
||||||
|
return rv
|
||||||
|
return _force_correct_text_reader(
|
||||||
|
sys.stdin, encoding, errors, force_readable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_text_stdout(encoding=None, errors=None):
|
||||||
|
rv = _get_windows_console_stream(sys.stdout, encoding, errors)
|
||||||
|
if rv is not None:
|
||||||
|
return rv
|
||||||
|
return _force_correct_text_writer(
|
||||||
|
sys.stdout, encoding, errors, force_writable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_text_stderr(encoding=None, errors=None):
|
||||||
|
rv = _get_windows_console_stream(sys.stderr, encoding, errors)
|
||||||
|
if rv is not None:
|
||||||
|
return rv
|
||||||
|
return _force_correct_text_writer(
|
||||||
|
sys.stderr, encoding, errors, force_writable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def filename_to_ui(value):
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
value = value.decode(get_filesystem_encoding(), "replace")
|
||||||
|
else:
|
||||||
|
value = value.encode("utf-8", "surrogateescape").decode("utf-8", "replace")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def get_streerror(e, default=None):
|
||||||
|
if hasattr(e, "strerror"):
|
||||||
|
msg = e.strerror
|
||||||
|
else:
|
||||||
|
if default is not None:
|
||||||
|
msg = default
|
||||||
|
else:
|
||||||
|
msg = str(e)
|
||||||
|
if isinstance(msg, bytes):
|
||||||
|
msg = msg.decode("utf-8", "replace")
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_io_open(file, mode, encoding, errors):
|
||||||
|
"""On Python 2, :func:`io.open` returns a text file wrapper that
|
||||||
|
requires passing ``unicode`` to ``write``. Need to open the file in
|
||||||
|
binary mode then wrap it in a subclass that can write ``str`` and
|
||||||
|
``unicode``.
|
||||||
|
|
||||||
|
Also handles not passing ``encoding`` and ``errors`` in binary mode.
|
||||||
|
"""
|
||||||
|
binary = "b" in mode
|
||||||
|
|
||||||
|
if binary:
|
||||||
|
kwargs = {}
|
||||||
|
else:
|
||||||
|
kwargs = {"encoding": encoding, "errors": errors}
|
||||||
|
|
||||||
|
if not PY2 or binary:
|
||||||
|
return io.open(file, mode, **kwargs)
|
||||||
|
|
||||||
|
f = io.open(file, "{}b".format(mode.replace("t", "")))
|
||||||
|
return _make_text_stream(f, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def open_stream(filename, mode="r", encoding=None, errors="strict", atomic=False):
|
||||||
|
binary = "b" in mode
|
||||||
|
|
||||||
|
# Standard streams first. These are simple because they don't need
|
||||||
|
# special handling for the atomic flag. It's entirely ignored.
|
||||||
|
if filename == "-":
|
||||||
|
if any(m in mode for m in ["w", "a", "x"]):
|
||||||
|
if binary:
|
||||||
|
return get_binary_stdout(), False
|
||||||
|
return get_text_stdout(encoding=encoding, errors=errors), False
|
||||||
|
if binary:
|
||||||
|
return get_binary_stdin(), False
|
||||||
|
return get_text_stdin(encoding=encoding, errors=errors), False
|
||||||
|
|
||||||
|
# Non-atomic writes directly go out through the regular open functions.
|
||||||
|
if not atomic:
|
||||||
|
return _wrap_io_open(filename, mode, encoding, errors), True
|
||||||
|
|
||||||
|
# Some usability stuff for atomic writes
|
||||||
|
if "a" in mode:
|
||||||
|
raise ValueError(
|
||||||
|
"Appending to an existing file is not supported, because that"
|
||||||
|
" would involve an expensive `copy`-operation to a temporary"
|
||||||
|
" file. Open the file in normal `w`-mode and copy explicitly"
|
||||||
|
" if that's what you're after."
|
||||||
|
)
|
||||||
|
if "x" in mode:
|
||||||
|
raise ValueError("Use the `overwrite`-parameter instead.")
|
||||||
|
if "w" not in mode:
|
||||||
|
raise ValueError("Atomic writes only make sense with `w`-mode.")
|
||||||
|
|
||||||
|
# Atomic writes are more complicated. They work by opening a file
|
||||||
|
# as a proxy in the same folder and then using the fdopen
|
||||||
|
# functionality to wrap it in a Python file. Then we wrap it in an
|
||||||
|
# atomic file that moves the file over on close.
|
||||||
|
import errno
|
||||||
|
import random
|
||||||
|
|
||||||
|
try:
|
||||||
|
perm = os.stat(filename).st_mode
|
||||||
|
except OSError:
|
||||||
|
perm = None
|
||||||
|
|
||||||
|
flags = os.O_RDWR | os.O_CREAT | os.O_EXCL
|
||||||
|
|
||||||
|
if binary:
|
||||||
|
flags |= getattr(os, "O_BINARY", 0)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
tmp_filename = os.path.join(
|
||||||
|
os.path.dirname(filename),
|
||||||
|
".__atomic-write{:08x}".format(random.randrange(1 << 32)),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm)
|
||||||
|
break
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno == errno.EEXIST or (
|
||||||
|
os.name == "nt"
|
||||||
|
and e.errno == errno.EACCES
|
||||||
|
and os.path.isdir(e.filename)
|
||||||
|
and os.access(e.filename, os.W_OK)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
|
||||||
|
if perm is not None:
|
||||||
|
os.chmod(tmp_filename, perm) # in case perm includes bits in umask
|
||||||
|
|
||||||
|
f = _wrap_io_open(fd, mode, encoding, errors)
|
||||||
|
return _AtomicFile(f, tmp_filename, os.path.realpath(filename)), True
|
||||||
|
|
||||||
|
|
||||||
|
# Used in a destructor call, needs extra protection from interpreter cleanup.
|
||||||
|
if hasattr(os, "replace"):
|
||||||
|
_replace = os.replace
|
||||||
|
_can_replace = True
|
||||||
|
else:
|
||||||
|
_replace = os.rename
|
||||||
|
_can_replace = not WIN
|
||||||
|
|
||||||
|
|
||||||
|
class _AtomicFile(object):
|
||||||
|
def __init__(self, f, tmp_filename, real_filename):
|
||||||
|
self._f = f
|
||||||
|
self._tmp_filename = tmp_filename
|
||||||
|
self._real_filename = real_filename
|
||||||
|
self.closed = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self._real_filename
|
||||||
|
|
||||||
|
def close(self, delete=False):
|
||||||
|
if self.closed:
|
||||||
|
return
|
||||||
|
self._f.close()
|
||||||
|
if not _can_replace:
|
||||||
|
try:
|
||||||
|
os.remove(self._real_filename)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
_replace(self._tmp_filename, self._real_filename)
|
||||||
|
self.closed = True
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self._f, name)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, tb):
|
||||||
|
self.close(delete=exc_type is not None)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return repr(self._f)
|
||||||
|
|
||||||
|
|
||||||
|
auto_wrap_for_ansi = None
|
||||||
|
colorama = None
|
||||||
|
get_winterm_size = None
|
||||||
|
|
||||||
|
|
||||||
|
def strip_ansi(value):
|
||||||
|
return _ansi_re.sub("", value)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_jupyter_kernel_output(stream):
|
||||||
|
if WIN:
|
||||||
|
# TODO: Couldn't test on Windows, should't try to support until
|
||||||
|
# someone tests the details wrt colorama.
|
||||||
|
return
|
||||||
|
|
||||||
|
while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)):
|
||||||
|
stream = stream._stream
|
||||||
|
|
||||||
|
return stream.__class__.__module__.startswith("ipykernel.")
|
||||||
|
|
||||||
|
|
||||||
|
def should_strip_ansi(stream=None, color=None):
|
||||||
|
if color is None:
|
||||||
|
if stream is None:
|
||||||
|
stream = sys.stdin
|
||||||
|
return not isatty(stream) and not _is_jupyter_kernel_output(stream)
|
||||||
|
return not color
|
||||||
|
|
||||||
|
|
||||||
|
# If we're on Windows, we provide transparent integration through
|
||||||
|
# colorama. This will make ANSI colors through the echo function
|
||||||
|
# work automatically.
|
||||||
|
if WIN:
|
||||||
|
# Windows has a smaller terminal
|
||||||
|
DEFAULT_COLUMNS = 79
|
||||||
|
|
||||||
|
from ._winconsole import _get_windows_console_stream, _wrap_std_stream
|
||||||
|
|
||||||
|
def _get_argv_encoding():
|
||||||
|
import locale
|
||||||
|
|
||||||
|
return locale.getpreferredencoding()
|
||||||
|
|
||||||
|
if PY2:
|
||||||
|
|
||||||
|
def raw_input(prompt=""):
|
||||||
|
sys.stderr.flush()
|
||||||
|
if prompt:
|
||||||
|
stdout = _default_text_stdout()
|
||||||
|
stdout.write(prompt)
|
||||||
|
stdin = _default_text_stdin()
|
||||||
|
return stdin.readline().rstrip("\r\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import colorama
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
_ansi_stream_wrappers = WeakKeyDictionary()
|
||||||
|
|
||||||
|
def auto_wrap_for_ansi(stream, color=None):
|
||||||
|
"""This function wraps a stream so that calls through colorama
|
||||||
|
are issued to the win32 console API to recolor on demand. It
|
||||||
|
also ensures to reset the colors if a write call is interrupted
|
||||||
|
to not destroy the console afterwards.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cached = _ansi_stream_wrappers.get(stream)
|
||||||
|
except Exception:
|
||||||
|
cached = None
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
strip = should_strip_ansi(stream, color)
|
||||||
|
ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip)
|
||||||
|
rv = ansi_wrapper.stream
|
||||||
|
_write = rv.write
|
||||||
|
|
||||||
|
def _safe_write(s):
|
||||||
|
try:
|
||||||
|
return _write(s)
|
||||||
|
except:
|
||||||
|
ansi_wrapper.reset_all()
|
||||||
|
raise
|
||||||
|
|
||||||
|
rv.write = _safe_write
|
||||||
|
try:
|
||||||
|
_ansi_stream_wrappers[stream] = rv
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def get_winterm_size():
|
||||||
|
win = colorama.win32.GetConsoleScreenBufferInfo(
|
||||||
|
colorama.win32.STDOUT
|
||||||
|
).srWindow
|
||||||
|
return win.Right - win.Left, win.Bottom - win.Top
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def _get_argv_encoding():
|
||||||
|
return getattr(sys.stdin, "encoding", None) or get_filesystem_encoding()
|
||||||
|
|
||||||
|
_get_windows_console_stream = lambda *x: None
|
||||||
|
_wrap_std_stream = lambda *x: None
|
||||||
|
|
||||||
|
|
||||||
|
def term_len(x):
|
||||||
|
return len(strip_ansi(x))
|
||||||
|
|
||||||
|
|
||||||
|
def isatty(stream):
|
||||||
|
try:
|
||||||
|
return stream.isatty()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _make_cached_stream_func(src_func, wrapper_func):
|
||||||
|
cache = WeakKeyDictionary()
|
||||||
|
|
||||||
|
def func():
|
||||||
|
stream = src_func()
|
||||||
|
try:
|
||||||
|
rv = cache.get(stream)
|
||||||
|
except Exception:
|
||||||
|
rv = None
|
||||||
|
if rv is not None:
|
||||||
|
return rv
|
||||||
|
rv = wrapper_func()
|
||||||
|
try:
|
||||||
|
stream = src_func() # In case wrapper_func() modified the stream
|
||||||
|
cache[stream] = rv
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return rv
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin)
|
||||||
|
_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout)
|
||||||
|
_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr)
|
||||||
|
|
||||||
|
|
||||||
|
binary_streams = {
|
||||||
|
"stdin": get_binary_stdin,
|
||||||
|
"stdout": get_binary_stdout,
|
||||||
|
"stderr": get_binary_stderr,
|
||||||
|
}
|
||||||
|
|
||||||
|
text_streams = {
|
||||||
|
"stdin": get_text_stdin,
|
||||||
|
"stdout": get_text_stdout,
|
||||||
|
"stderr": get_text_stderr,
|
||||||
|
}
|
||||||
657
mixly/tools/python/click/_termui_impl.py
Normal file
@@ -0,0 +1,657 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This module contains implementations for the termui module. To keep the
|
||||||
|
import time of Click down, some infrequently used functionality is
|
||||||
|
placed in this module and only imported as needed.
|
||||||
|
"""
|
||||||
|
import contextlib
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from ._compat import _default_text_stdout
|
||||||
|
from ._compat import CYGWIN
|
||||||
|
from ._compat import get_best_encoding
|
||||||
|
from ._compat import int_types
|
||||||
|
from ._compat import isatty
|
||||||
|
from ._compat import open_stream
|
||||||
|
from ._compat import range_type
|
||||||
|
from ._compat import strip_ansi
|
||||||
|
from ._compat import term_len
|
||||||
|
from ._compat import WIN
|
||||||
|
from .exceptions import ClickException
|
||||||
|
from .utils import echo
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
|
BEFORE_BAR = "\r"
|
||||||
|
AFTER_BAR = "\n"
|
||||||
|
else:
|
||||||
|
BEFORE_BAR = "\r\033[?25l"
|
||||||
|
AFTER_BAR = "\033[?25h\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _length_hint(obj):
|
||||||
|
"""Returns the length hint of an object."""
|
||||||
|
try:
|
||||||
|
return len(obj)
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
try:
|
||||||
|
get_hint = type(obj).__length_hint__
|
||||||
|
except AttributeError:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
hint = get_hint(obj)
|
||||||
|
except TypeError:
|
||||||
|
return None
|
||||||
|
if hint is NotImplemented or not isinstance(hint, int_types) or hint < 0:
|
||||||
|
return None
|
||||||
|
return hint
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressBar(object):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
iterable,
|
||||||
|
length=None,
|
||||||
|
fill_char="#",
|
||||||
|
empty_char=" ",
|
||||||
|
bar_template="%(bar)s",
|
||||||
|
info_sep=" ",
|
||||||
|
show_eta=True,
|
||||||
|
show_percent=None,
|
||||||
|
show_pos=False,
|
||||||
|
item_show_func=None,
|
||||||
|
label=None,
|
||||||
|
file=None,
|
||||||
|
color=None,
|
||||||
|
width=30,
|
||||||
|
):
|
||||||
|
self.fill_char = fill_char
|
||||||
|
self.empty_char = empty_char
|
||||||
|
self.bar_template = bar_template
|
||||||
|
self.info_sep = info_sep
|
||||||
|
self.show_eta = show_eta
|
||||||
|
self.show_percent = show_percent
|
||||||
|
self.show_pos = show_pos
|
||||||
|
self.item_show_func = item_show_func
|
||||||
|
self.label = label or ""
|
||||||
|
if file is None:
|
||||||
|
file = _default_text_stdout()
|
||||||
|
self.file = file
|
||||||
|
self.color = color
|
||||||
|
self.width = width
|
||||||
|
self.autowidth = width == 0
|
||||||
|
|
||||||
|
if length is None:
|
||||||
|
length = _length_hint(iterable)
|
||||||
|
if iterable is None:
|
||||||
|
if length is None:
|
||||||
|
raise TypeError("iterable or length is required")
|
||||||
|
iterable = range_type(length)
|
||||||
|
self.iter = iter(iterable)
|
||||||
|
self.length = length
|
||||||
|
self.length_known = length is not None
|
||||||
|
self.pos = 0
|
||||||
|
self.avg = []
|
||||||
|
self.start = self.last_eta = time.time()
|
||||||
|
self.eta_known = False
|
||||||
|
self.finished = False
|
||||||
|
self.max_width = None
|
||||||
|
self.entered = False
|
||||||
|
self.current_item = None
|
||||||
|
self.is_hidden = not isatty(self.file)
|
||||||
|
self._last_line = None
|
||||||
|
self.short_limit = 0.5
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.entered = True
|
||||||
|
self.render_progress()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, tb):
|
||||||
|
self.render_finish()
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
if not self.entered:
|
||||||
|
raise RuntimeError("You need to use progress bars in a with block.")
|
||||||
|
self.render_progress()
|
||||||
|
return self.generator()
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
# Iteration is defined in terms of a generator function,
|
||||||
|
# returned by iter(self); use that to define next(). This works
|
||||||
|
# because `self.iter` is an iterable consumed by that generator,
|
||||||
|
# so it is re-entry safe. Calling `next(self.generator())`
|
||||||
|
# twice works and does "what you want".
|
||||||
|
return next(iter(self))
|
||||||
|
|
||||||
|
# Python 2 compat
|
||||||
|
next = __next__
|
||||||
|
|
||||||
|
def is_fast(self):
|
||||||
|
return time.time() - self.start <= self.short_limit
|
||||||
|
|
||||||
|
def render_finish(self):
|
||||||
|
if self.is_hidden or self.is_fast():
|
||||||
|
return
|
||||||
|
self.file.write(AFTER_BAR)
|
||||||
|
self.file.flush()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pct(self):
|
||||||
|
if self.finished:
|
||||||
|
return 1.0
|
||||||
|
return min(self.pos / (float(self.length) or 1), 1.0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_per_iteration(self):
|
||||||
|
if not self.avg:
|
||||||
|
return 0.0
|
||||||
|
return sum(self.avg) / float(len(self.avg))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def eta(self):
|
||||||
|
if self.length_known and not self.finished:
|
||||||
|
return self.time_per_iteration * (self.length - self.pos)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def format_eta(self):
|
||||||
|
if self.eta_known:
|
||||||
|
t = int(self.eta)
|
||||||
|
seconds = t % 60
|
||||||
|
t //= 60
|
||||||
|
minutes = t % 60
|
||||||
|
t //= 60
|
||||||
|
hours = t % 24
|
||||||
|
t //= 24
|
||||||
|
if t > 0:
|
||||||
|
return "{}d {:02}:{:02}:{:02}".format(t, hours, minutes, seconds)
|
||||||
|
else:
|
||||||
|
return "{:02}:{:02}:{:02}".format(hours, minutes, seconds)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def format_pos(self):
|
||||||
|
pos = str(self.pos)
|
||||||
|
if self.length_known:
|
||||||
|
pos += "/{}".format(self.length)
|
||||||
|
return pos
|
||||||
|
|
||||||
|
def format_pct(self):
|
||||||
|
return "{: 4}%".format(int(self.pct * 100))[1:]
|
||||||
|
|
||||||
|
def format_bar(self):
|
||||||
|
if self.length_known:
|
||||||
|
bar_length = int(self.pct * self.width)
|
||||||
|
bar = self.fill_char * bar_length
|
||||||
|
bar += self.empty_char * (self.width - bar_length)
|
||||||
|
elif self.finished:
|
||||||
|
bar = self.fill_char * self.width
|
||||||
|
else:
|
||||||
|
bar = list(self.empty_char * (self.width or 1))
|
||||||
|
if self.time_per_iteration != 0:
|
||||||
|
bar[
|
||||||
|
int(
|
||||||
|
(math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5)
|
||||||
|
* self.width
|
||||||
|
)
|
||||||
|
] = self.fill_char
|
||||||
|
bar = "".join(bar)
|
||||||
|
return bar
|
||||||
|
|
||||||
|
def format_progress_line(self):
|
||||||
|
show_percent = self.show_percent
|
||||||
|
|
||||||
|
info_bits = []
|
||||||
|
if self.length_known and show_percent is None:
|
||||||
|
show_percent = not self.show_pos
|
||||||
|
|
||||||
|
if self.show_pos:
|
||||||
|
info_bits.append(self.format_pos())
|
||||||
|
if show_percent:
|
||||||
|
info_bits.append(self.format_pct())
|
||||||
|
if self.show_eta and self.eta_known and not self.finished:
|
||||||
|
info_bits.append(self.format_eta())
|
||||||
|
if self.item_show_func is not None:
|
||||||
|
item_info = self.item_show_func(self.current_item)
|
||||||
|
if item_info is not None:
|
||||||
|
info_bits.append(item_info)
|
||||||
|
|
||||||
|
return (
|
||||||
|
self.bar_template
|
||||||
|
% {
|
||||||
|
"label": self.label,
|
||||||
|
"bar": self.format_bar(),
|
||||||
|
"info": self.info_sep.join(info_bits),
|
||||||
|
}
|
||||||
|
).rstrip()
|
||||||
|
|
||||||
|
def render_progress(self):
|
||||||
|
from .termui import get_terminal_size
|
||||||
|
|
||||||
|
if self.is_hidden:
|
||||||
|
return
|
||||||
|
|
||||||
|
buf = []
|
||||||
|
# Update width in case the terminal has been resized
|
||||||
|
if self.autowidth:
|
||||||
|
old_width = self.width
|
||||||
|
self.width = 0
|
||||||
|
clutter_length = term_len(self.format_progress_line())
|
||||||
|
new_width = max(0, get_terminal_size()[0] - clutter_length)
|
||||||
|
if new_width < old_width:
|
||||||
|
buf.append(BEFORE_BAR)
|
||||||
|
buf.append(" " * self.max_width)
|
||||||
|
self.max_width = new_width
|
||||||
|
self.width = new_width
|
||||||
|
|
||||||
|
clear_width = self.width
|
||||||
|
if self.max_width is not None:
|
||||||
|
clear_width = self.max_width
|
||||||
|
|
||||||
|
buf.append(BEFORE_BAR)
|
||||||
|
line = self.format_progress_line()
|
||||||
|
line_len = term_len(line)
|
||||||
|
if self.max_width is None or self.max_width < line_len:
|
||||||
|
self.max_width = line_len
|
||||||
|
|
||||||
|
buf.append(line)
|
||||||
|
buf.append(" " * (clear_width - line_len))
|
||||||
|
line = "".join(buf)
|
||||||
|
# Render the line only if it changed.
|
||||||
|
|
||||||
|
if line != self._last_line and not self.is_fast():
|
||||||
|
self._last_line = line
|
||||||
|
echo(line, file=self.file, color=self.color, nl=False)
|
||||||
|
self.file.flush()
|
||||||
|
|
||||||
|
def make_step(self, n_steps):
|
||||||
|
self.pos += n_steps
|
||||||
|
if self.length_known and self.pos >= self.length:
|
||||||
|
self.finished = True
|
||||||
|
|
||||||
|
if (time.time() - self.last_eta) < 1.0:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.last_eta = time.time()
|
||||||
|
|
||||||
|
# self.avg is a rolling list of length <= 7 of steps where steps are
|
||||||
|
# defined as time elapsed divided by the total progress through
|
||||||
|
# self.length.
|
||||||
|
if self.pos:
|
||||||
|
step = (time.time() - self.start) / self.pos
|
||||||
|
else:
|
||||||
|
step = time.time() - self.start
|
||||||
|
|
||||||
|
self.avg = self.avg[-6:] + [step]
|
||||||
|
|
||||||
|
self.eta_known = self.length_known
|
||||||
|
|
||||||
|
def update(self, n_steps):
|
||||||
|
self.make_step(n_steps)
|
||||||
|
self.render_progress()
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
self.eta_known = 0
|
||||||
|
self.current_item = None
|
||||||
|
self.finished = True
|
||||||
|
|
||||||
|
def generator(self):
|
||||||
|
"""Return a generator which yields the items added to the bar
|
||||||
|
during construction, and updates the progress bar *after* the
|
||||||
|
yielded block returns.
|
||||||
|
"""
|
||||||
|
# WARNING: the iterator interface for `ProgressBar` relies on
|
||||||
|
# this and only works because this is a simple generator which
|
||||||
|
# doesn't create or manage additional state. If this function
|
||||||
|
# changes, the impact should be evaluated both against
|
||||||
|
# `iter(bar)` and `next(bar)`. `next()` in particular may call
|
||||||
|
# `self.generator()` repeatedly, and this must remain safe in
|
||||||
|
# order for that interface to work.
|
||||||
|
if not self.entered:
|
||||||
|
raise RuntimeError("You need to use progress bars in a with block.")
|
||||||
|
|
||||||
|
if self.is_hidden:
|
||||||
|
for rv in self.iter:
|
||||||
|
yield rv
|
||||||
|
else:
|
||||||
|
for rv in self.iter:
|
||||||
|
self.current_item = rv
|
||||||
|
yield rv
|
||||||
|
self.update(1)
|
||||||
|
self.finish()
|
||||||
|
self.render_progress()
|
||||||
|
|
||||||
|
|
||||||
|
def pager(generator, color=None):
|
||||||
|
"""Decide what method to use for paging through text."""
|
||||||
|
stdout = _default_text_stdout()
|
||||||
|
if not isatty(sys.stdin) or not isatty(stdout):
|
||||||
|
return _nullpager(stdout, generator, color)
|
||||||
|
pager_cmd = (os.environ.get("PAGER", None) or "").strip()
|
||||||
|
if pager_cmd:
|
||||||
|
if WIN:
|
||||||
|
return _tempfilepager(generator, pager_cmd, color)
|
||||||
|
return _pipepager(generator, pager_cmd, color)
|
||||||
|
if os.environ.get("TERM") in ("dumb", "emacs"):
|
||||||
|
return _nullpager(stdout, generator, color)
|
||||||
|
if WIN or sys.platform.startswith("os2"):
|
||||||
|
return _tempfilepager(generator, "more <", color)
|
||||||
|
if hasattr(os, "system") and os.system("(less) 2>/dev/null") == 0:
|
||||||
|
return _pipepager(generator, "less", color)
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
fd, filename = tempfile.mkstemp()
|
||||||
|
os.close(fd)
|
||||||
|
try:
|
||||||
|
if hasattr(os, "system") and os.system('more "{}"'.format(filename)) == 0:
|
||||||
|
return _pipepager(generator, "more", color)
|
||||||
|
return _nullpager(stdout, generator, color)
|
||||||
|
finally:
|
||||||
|
os.unlink(filename)
|
||||||
|
|
||||||
|
|
||||||
|
def _pipepager(generator, cmd, color):
|
||||||
|
"""Page through text by feeding it to another program. Invoking a
|
||||||
|
pager through this might support colors.
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
env = dict(os.environ)
|
||||||
|
|
||||||
|
# If we're piping to less we might support colors under the
|
||||||
|
# condition that
|
||||||
|
cmd_detail = cmd.rsplit("/", 1)[-1].split()
|
||||||
|
if color is None and cmd_detail[0] == "less":
|
||||||
|
less_flags = "{}{}".format(os.environ.get("LESS", ""), " ".join(cmd_detail[1:]))
|
||||||
|
if not less_flags:
|
||||||
|
env["LESS"] = "-R"
|
||||||
|
color = True
|
||||||
|
elif "r" in less_flags or "R" in less_flags:
|
||||||
|
color = True
|
||||||
|
|
||||||
|
c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env)
|
||||||
|
encoding = get_best_encoding(c.stdin)
|
||||||
|
try:
|
||||||
|
for text in generator:
|
||||||
|
if not color:
|
||||||
|
text = strip_ansi(text)
|
||||||
|
|
||||||
|
c.stdin.write(text.encode(encoding, "replace"))
|
||||||
|
except (IOError, KeyboardInterrupt):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
c.stdin.close()
|
||||||
|
|
||||||
|
# Less doesn't respect ^C, but catches it for its own UI purposes (aborting
|
||||||
|
# search or other commands inside less).
|
||||||
|
#
|
||||||
|
# That means when the user hits ^C, the parent process (click) terminates,
|
||||||
|
# but less is still alive, paging the output and messing up the terminal.
|
||||||
|
#
|
||||||
|
# If the user wants to make the pager exit on ^C, they should set
|
||||||
|
# `LESS='-K'`. It's not our decision to make.
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
c.wait()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def _tempfilepager(generator, cmd, color):
|
||||||
|
"""Page through text by invoking a program on a temporary file."""
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
filename = tempfile.mktemp()
|
||||||
|
# TODO: This never terminates if the passed generator never terminates.
|
||||||
|
text = "".join(generator)
|
||||||
|
if not color:
|
||||||
|
text = strip_ansi(text)
|
||||||
|
encoding = get_best_encoding(sys.stdout)
|
||||||
|
with open_stream(filename, "wb")[0] as f:
|
||||||
|
f.write(text.encode(encoding))
|
||||||
|
try:
|
||||||
|
os.system('{} "{}"'.format(cmd, filename))
|
||||||
|
finally:
|
||||||
|
os.unlink(filename)
|
||||||
|
|
||||||
|
|
||||||
|
def _nullpager(stream, generator, color):
|
||||||
|
"""Simply print unformatted text. This is the ultimate fallback."""
|
||||||
|
for text in generator:
|
||||||
|
if not color:
|
||||||
|
text = strip_ansi(text)
|
||||||
|
stream.write(text)
|
||||||
|
|
||||||
|
|
||||||
|
class Editor(object):
|
||||||
|
def __init__(self, editor=None, env=None, require_save=True, extension=".txt"):
|
||||||
|
self.editor = editor
|
||||||
|
self.env = env
|
||||||
|
self.require_save = require_save
|
||||||
|
self.extension = extension
|
||||||
|
|
||||||
|
def get_editor(self):
|
||||||
|
if self.editor is not None:
|
||||||
|
return self.editor
|
||||||
|
for key in "VISUAL", "EDITOR":
|
||||||
|
rv = os.environ.get(key)
|
||||||
|
if rv:
|
||||||
|
return rv
|
||||||
|
if WIN:
|
||||||
|
return "notepad"
|
||||||
|
for editor in "sensible-editor", "vim", "nano":
|
||||||
|
if os.system("which {} >/dev/null 2>&1".format(editor)) == 0:
|
||||||
|
return editor
|
||||||
|
return "vi"
|
||||||
|
|
||||||
|
def edit_file(self, filename):
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
editor = self.get_editor()
|
||||||
|
if self.env:
|
||||||
|
environ = os.environ.copy()
|
||||||
|
environ.update(self.env)
|
||||||
|
else:
|
||||||
|
environ = None
|
||||||
|
try:
|
||||||
|
c = subprocess.Popen(
|
||||||
|
'{} "{}"'.format(editor, filename), env=environ, shell=True,
|
||||||
|
)
|
||||||
|
exit_code = c.wait()
|
||||||
|
if exit_code != 0:
|
||||||
|
raise ClickException("{}: Editing failed!".format(editor))
|
||||||
|
except OSError as e:
|
||||||
|
raise ClickException("{}: Editing failed: {}".format(editor, e))
|
||||||
|
|
||||||
|
def edit(self, text):
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
text = text or ""
|
||||||
|
if text and not text.endswith("\n"):
|
||||||
|
text += "\n"
|
||||||
|
|
||||||
|
fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension)
|
||||||
|
try:
|
||||||
|
if WIN:
|
||||||
|
encoding = "utf-8-sig"
|
||||||
|
text = text.replace("\n", "\r\n")
|
||||||
|
else:
|
||||||
|
encoding = "utf-8"
|
||||||
|
text = text.encode(encoding)
|
||||||
|
|
||||||
|
f = os.fdopen(fd, "wb")
|
||||||
|
f.write(text)
|
||||||
|
f.close()
|
||||||
|
timestamp = os.path.getmtime(name)
|
||||||
|
|
||||||
|
self.edit_file(name)
|
||||||
|
|
||||||
|
if self.require_save and os.path.getmtime(name) == timestamp:
|
||||||
|
return None
|
||||||
|
|
||||||
|
f = open(name, "rb")
|
||||||
|
try:
|
||||||
|
rv = f.read()
|
||||||
|
finally:
|
||||||
|
f.close()
|
||||||
|
return rv.decode("utf-8-sig").replace("\r\n", "\n")
|
||||||
|
finally:
|
||||||
|
os.unlink(name)
|
||||||
|
|
||||||
|
|
||||||
|
def open_url(url, wait=False, locate=False):
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def _unquote_file(url):
|
||||||
|
try:
|
||||||
|
import urllib
|
||||||
|
except ImportError:
|
||||||
|
import urllib
|
||||||
|
if url.startswith("file://"):
|
||||||
|
url = urllib.unquote(url[7:])
|
||||||
|
return url
|
||||||
|
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
args = ["open"]
|
||||||
|
if wait:
|
||||||
|
args.append("-W")
|
||||||
|
if locate:
|
||||||
|
args.append("-R")
|
||||||
|
args.append(_unquote_file(url))
|
||||||
|
null = open("/dev/null", "w")
|
||||||
|
try:
|
||||||
|
return subprocess.Popen(args, stderr=null).wait()
|
||||||
|
finally:
|
||||||
|
null.close()
|
||||||
|
elif WIN:
|
||||||
|
if locate:
|
||||||
|
url = _unquote_file(url)
|
||||||
|
args = 'explorer /select,"{}"'.format(_unquote_file(url.replace('"', "")))
|
||||||
|
else:
|
||||||
|
args = 'start {} "" "{}"'.format(
|
||||||
|
"/WAIT" if wait else "", url.replace('"', "")
|
||||||
|
)
|
||||||
|
return os.system(args)
|
||||||
|
elif CYGWIN:
|
||||||
|
if locate:
|
||||||
|
url = _unquote_file(url)
|
||||||
|
args = 'cygstart "{}"'.format(os.path.dirname(url).replace('"', ""))
|
||||||
|
else:
|
||||||
|
args = 'cygstart {} "{}"'.format("-w" if wait else "", url.replace('"', ""))
|
||||||
|
return os.system(args)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if locate:
|
||||||
|
url = os.path.dirname(_unquote_file(url)) or "."
|
||||||
|
else:
|
||||||
|
url = _unquote_file(url)
|
||||||
|
c = subprocess.Popen(["xdg-open", url])
|
||||||
|
if wait:
|
||||||
|
return c.wait()
|
||||||
|
return 0
|
||||||
|
except OSError:
|
||||||
|
if url.startswith(("http://", "https://")) and not locate and not wait:
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
|
webbrowser.open(url)
|
||||||
|
return 0
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def _translate_ch_to_exc(ch):
|
||||||
|
if ch == u"\x03":
|
||||||
|
raise KeyboardInterrupt()
|
||||||
|
if ch == u"\x04" and not WIN: # Unix-like, Ctrl+D
|
||||||
|
raise EOFError()
|
||||||
|
if ch == u"\x1a" and WIN: # Windows, Ctrl+Z
|
||||||
|
raise EOFError()
|
||||||
|
|
||||||
|
|
||||||
|
if WIN:
|
||||||
|
import msvcrt
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def raw_terminal():
|
||||||
|
yield
|
||||||
|
|
||||||
|
def getchar(echo):
|
||||||
|
# The function `getch` will return a bytes object corresponding to
|
||||||
|
# the pressed character. Since Windows 10 build 1803, it will also
|
||||||
|
# return \x00 when called a second time after pressing a regular key.
|
||||||
|
#
|
||||||
|
# `getwch` does not share this probably-bugged behavior. Moreover, it
|
||||||
|
# returns a Unicode object by default, which is what we want.
|
||||||
|
#
|
||||||
|
# Either of these functions will return \x00 or \xe0 to indicate
|
||||||
|
# a special key, and you need to call the same function again to get
|
||||||
|
# the "rest" of the code. The fun part is that \u00e0 is
|
||||||
|
# "latin small letter a with grave", so if you type that on a French
|
||||||
|
# keyboard, you _also_ get a \xe0.
|
||||||
|
# E.g., consider the Up arrow. This returns \xe0 and then \x48. The
|
||||||
|
# resulting Unicode string reads as "a with grave" + "capital H".
|
||||||
|
# This is indistinguishable from when the user actually types
|
||||||
|
# "a with grave" and then "capital H".
|
||||||
|
#
|
||||||
|
# When \xe0 is returned, we assume it's part of a special-key sequence
|
||||||
|
# and call `getwch` again, but that means that when the user types
|
||||||
|
# the \u00e0 character, `getchar` doesn't return until a second
|
||||||
|
# character is typed.
|
||||||
|
# The alternative is returning immediately, but that would mess up
|
||||||
|
# cross-platform handling of arrow keys and others that start with
|
||||||
|
# \xe0. Another option is using `getch`, but then we can't reliably
|
||||||
|
# read non-ASCII characters, because return values of `getch` are
|
||||||
|
# limited to the current 8-bit codepage.
|
||||||
|
#
|
||||||
|
# Anyway, Click doesn't claim to do this Right(tm), and using `getwch`
|
||||||
|
# is doing the right thing in more situations than with `getch`.
|
||||||
|
if echo:
|
||||||
|
func = msvcrt.getwche
|
||||||
|
else:
|
||||||
|
func = msvcrt.getwch
|
||||||
|
|
||||||
|
rv = func()
|
||||||
|
if rv in (u"\x00", u"\xe0"):
|
||||||
|
# \x00 and \xe0 are control characters that indicate special key,
|
||||||
|
# see above.
|
||||||
|
rv += func()
|
||||||
|
_translate_ch_to_exc(rv)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
import tty
|
||||||
|
import termios
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def raw_terminal():
|
||||||
|
if not isatty(sys.stdin):
|
||||||
|
f = open("/dev/tty")
|
||||||
|
fd = f.fileno()
|
||||||
|
else:
|
||||||
|
fd = sys.stdin.fileno()
|
||||||
|
f = None
|
||||||
|
try:
|
||||||
|
old_settings = termios.tcgetattr(fd)
|
||||||
|
try:
|
||||||
|
tty.setraw(fd)
|
||||||
|
yield fd
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||||
|
sys.stdout.flush()
|
||||||
|
if f is not None:
|
||||||
|
f.close()
|
||||||
|
except termios.error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getchar(echo):
|
||||||
|
with raw_terminal() as fd:
|
||||||
|
ch = os.read(fd, 32)
|
||||||
|
ch = ch.decode(get_best_encoding(sys.stdin), "replace")
|
||||||
|
if echo and isatty(sys.stdout):
|
||||||
|
sys.stdout.write(ch)
|
||||||
|
_translate_ch_to_exc(ch)
|
||||||
|
return ch
|
||||||
37
mixly/tools/python/click/_textwrap.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import textwrap
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
|
||||||
|
class TextWrapper(textwrap.TextWrapper):
|
||||||
|
def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
|
||||||
|
space_left = max(width - cur_len, 1)
|
||||||
|
|
||||||
|
if self.break_long_words:
|
||||||
|
last = reversed_chunks[-1]
|
||||||
|
cut = last[:space_left]
|
||||||
|
res = last[space_left:]
|
||||||
|
cur_line.append(cut)
|
||||||
|
reversed_chunks[-1] = res
|
||||||
|
elif not cur_line:
|
||||||
|
cur_line.append(reversed_chunks.pop())
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def extra_indent(self, indent):
|
||||||
|
old_initial_indent = self.initial_indent
|
||||||
|
old_subsequent_indent = self.subsequent_indent
|
||||||
|
self.initial_indent += indent
|
||||||
|
self.subsequent_indent += indent
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self.initial_indent = old_initial_indent
|
||||||
|
self.subsequent_indent = old_subsequent_indent
|
||||||
|
|
||||||
|
def indent_only(self, text):
|
||||||
|
rv = []
|
||||||
|
for idx, line in enumerate(text.splitlines()):
|
||||||
|
indent = self.initial_indent
|
||||||
|
if idx > 0:
|
||||||
|
indent = self.subsequent_indent
|
||||||
|
rv.append(indent + line)
|
||||||
|
return "\n".join(rv)
|
||||||
131
mixly/tools/python/click/_unicodefun.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import codecs
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ._compat import PY2
|
||||||
|
|
||||||
|
|
||||||
|
def _find_unicode_literals_frame():
|
||||||
|
import __future__
|
||||||
|
|
||||||
|
if not hasattr(sys, "_getframe"): # not all Python implementations have it
|
||||||
|
return 0
|
||||||
|
frm = sys._getframe(1)
|
||||||
|
idx = 1
|
||||||
|
while frm is not None:
|
||||||
|
if frm.f_globals.get("__name__", "").startswith("click."):
|
||||||
|
frm = frm.f_back
|
||||||
|
idx += 1
|
||||||
|
elif frm.f_code.co_flags & __future__.unicode_literals.compiler_flag:
|
||||||
|
return idx
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _check_for_unicode_literals():
|
||||||
|
if not __debug__:
|
||||||
|
return
|
||||||
|
|
||||||
|
from . import disable_unicode_literals_warning
|
||||||
|
|
||||||
|
if not PY2 or disable_unicode_literals_warning:
|
||||||
|
return
|
||||||
|
bad_frame = _find_unicode_literals_frame()
|
||||||
|
if bad_frame <= 0:
|
||||||
|
return
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
warn(
|
||||||
|
Warning(
|
||||||
|
"Click detected the use of the unicode_literals __future__"
|
||||||
|
" import. This is heavily discouraged because it can"
|
||||||
|
" introduce subtle bugs in your code. You should instead"
|
||||||
|
' use explicit u"" literals for your unicode strings. For'
|
||||||
|
" more information see"
|
||||||
|
" https://click.palletsprojects.com/python3/"
|
||||||
|
),
|
||||||
|
stacklevel=bad_frame,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_python3_env():
|
||||||
|
"""Ensures that the environment is good for unicode on Python 3."""
|
||||||
|
if PY2:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
import locale
|
||||||
|
|
||||||
|
fs_enc = codecs.lookup(locale.getpreferredencoding()).name
|
||||||
|
except Exception:
|
||||||
|
fs_enc = "ascii"
|
||||||
|
if fs_enc != "ascii":
|
||||||
|
return
|
||||||
|
|
||||||
|
extra = ""
|
||||||
|
if os.name == "posix":
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
try:
|
||||||
|
rv = subprocess.Popen(
|
||||||
|
["locale", "-a"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||||
|
).communicate()[0]
|
||||||
|
except OSError:
|
||||||
|
rv = b""
|
||||||
|
good_locales = set()
|
||||||
|
has_c_utf8 = False
|
||||||
|
|
||||||
|
# Make sure we're operating on text here.
|
||||||
|
if isinstance(rv, bytes):
|
||||||
|
rv = rv.decode("ascii", "replace")
|
||||||
|
|
||||||
|
for line in rv.splitlines():
|
||||||
|
locale = line.strip()
|
||||||
|
if locale.lower().endswith((".utf-8", ".utf8")):
|
||||||
|
good_locales.add(locale)
|
||||||
|
if locale.lower() in ("c.utf8", "c.utf-8"):
|
||||||
|
has_c_utf8 = True
|
||||||
|
|
||||||
|
extra += "\n\n"
|
||||||
|
if not good_locales:
|
||||||
|
extra += (
|
||||||
|
"Additional information: on this system no suitable"
|
||||||
|
" UTF-8 locales were discovered. This most likely"
|
||||||
|
" requires resolving by reconfiguring the locale"
|
||||||
|
" system."
|
||||||
|
)
|
||||||
|
elif has_c_utf8:
|
||||||
|
extra += (
|
||||||
|
"This system supports the C.UTF-8 locale which is"
|
||||||
|
" recommended. You might be able to resolve your issue"
|
||||||
|
" by exporting the following environment variables:\n\n"
|
||||||
|
" export LC_ALL=C.UTF-8\n"
|
||||||
|
" export LANG=C.UTF-8"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
extra += (
|
||||||
|
"This system lists a couple of UTF-8 supporting locales"
|
||||||
|
" that you can pick from. The following suitable"
|
||||||
|
" locales were discovered: {}".format(", ".join(sorted(good_locales)))
|
||||||
|
)
|
||||||
|
|
||||||
|
bad_locale = None
|
||||||
|
for locale in os.environ.get("LC_ALL"), os.environ.get("LANG"):
|
||||||
|
if locale and locale.lower().endswith((".utf-8", ".utf8")):
|
||||||
|
bad_locale = locale
|
||||||
|
if locale is not None:
|
||||||
|
break
|
||||||
|
if bad_locale is not None:
|
||||||
|
extra += (
|
||||||
|
"\n\nClick discovered that you exported a UTF-8 locale"
|
||||||
|
" but the locale system could not pick up from it"
|
||||||
|
" because it does not exist. The exported locale is"
|
||||||
|
" '{}' but it is not supported".format(bad_locale)
|
||||||
|
)
|
||||||
|
|
||||||
|
raise RuntimeError(
|
||||||
|
"Click will abort further execution because Python 3 was"
|
||||||
|
" configured to use ASCII as encoding for the environment."
|
||||||
|
" Consult https://click.palletsprojects.com/python3/ for"
|
||||||
|
" mitigation steps.{}".format(extra)
|
||||||
|
)
|
||||||
370
mixly/tools/python/click/_winconsole.py
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# This module is based on the excellent work by Adam Bartoš who
|
||||||
|
# provided a lot of what went into the implementation here in
|
||||||
|
# the discussion to issue1602 in the Python bug tracker.
|
||||||
|
#
|
||||||
|
# There are some general differences in regards to how this works
|
||||||
|
# compared to the original patches as we do not need to patch
|
||||||
|
# the entire interpreter but just work in our little world of
|
||||||
|
# echo and prmopt.
|
||||||
|
import ctypes
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import zlib
|
||||||
|
from ctypes import byref
|
||||||
|
from ctypes import c_char
|
||||||
|
from ctypes import c_char_p
|
||||||
|
from ctypes import c_int
|
||||||
|
from ctypes import c_ssize_t
|
||||||
|
from ctypes import c_ulong
|
||||||
|
from ctypes import c_void_p
|
||||||
|
from ctypes import POINTER
|
||||||
|
from ctypes import py_object
|
||||||
|
from ctypes import windll
|
||||||
|
from ctypes import WinError
|
||||||
|
from ctypes import WINFUNCTYPE
|
||||||
|
from ctypes.wintypes import DWORD
|
||||||
|
from ctypes.wintypes import HANDLE
|
||||||
|
from ctypes.wintypes import LPCWSTR
|
||||||
|
from ctypes.wintypes import LPWSTR
|
||||||
|
|
||||||
|
import msvcrt
|
||||||
|
|
||||||
|
from ._compat import _NonClosingTextIOWrapper
|
||||||
|
from ._compat import PY2
|
||||||
|
from ._compat import text_type
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ctypes import pythonapi
|
||||||
|
|
||||||
|
PyObject_GetBuffer = pythonapi.PyObject_GetBuffer
|
||||||
|
PyBuffer_Release = pythonapi.PyBuffer_Release
|
||||||
|
except ImportError:
|
||||||
|
pythonapi = None
|
||||||
|
|
||||||
|
|
||||||
|
c_ssize_p = POINTER(c_ssize_t)
|
||||||
|
|
||||||
|
kernel32 = windll.kernel32
|
||||||
|
GetStdHandle = kernel32.GetStdHandle
|
||||||
|
ReadConsoleW = kernel32.ReadConsoleW
|
||||||
|
WriteConsoleW = kernel32.WriteConsoleW
|
||||||
|
GetConsoleMode = kernel32.GetConsoleMode
|
||||||
|
GetLastError = kernel32.GetLastError
|
||||||
|
GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32))
|
||||||
|
CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(
|
||||||
|
("CommandLineToArgvW", windll.shell32)
|
||||||
|
)
|
||||||
|
LocalFree = WINFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)(
|
||||||
|
("LocalFree", windll.kernel32)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
STDIN_HANDLE = GetStdHandle(-10)
|
||||||
|
STDOUT_HANDLE = GetStdHandle(-11)
|
||||||
|
STDERR_HANDLE = GetStdHandle(-12)
|
||||||
|
|
||||||
|
|
||||||
|
PyBUF_SIMPLE = 0
|
||||||
|
PyBUF_WRITABLE = 1
|
||||||
|
|
||||||
|
ERROR_SUCCESS = 0
|
||||||
|
ERROR_NOT_ENOUGH_MEMORY = 8
|
||||||
|
ERROR_OPERATION_ABORTED = 995
|
||||||
|
|
||||||
|
STDIN_FILENO = 0
|
||||||
|
STDOUT_FILENO = 1
|
||||||
|
STDERR_FILENO = 2
|
||||||
|
|
||||||
|
EOF = b"\x1a"
|
||||||
|
MAX_BYTES_WRITTEN = 32767
|
||||||
|
|
||||||
|
|
||||||
|
class Py_buffer(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
("buf", c_void_p),
|
||||||
|
("obj", py_object),
|
||||||
|
("len", c_ssize_t),
|
||||||
|
("itemsize", c_ssize_t),
|
||||||
|
("readonly", c_int),
|
||||||
|
("ndim", c_int),
|
||||||
|
("format", c_char_p),
|
||||||
|
("shape", c_ssize_p),
|
||||||
|
("strides", c_ssize_p),
|
||||||
|
("suboffsets", c_ssize_p),
|
||||||
|
("internal", c_void_p),
|
||||||
|
]
|
||||||
|
|
||||||
|
if PY2:
|
||||||
|
_fields_.insert(-1, ("smalltable", c_ssize_t * 2))
|
||||||
|
|
||||||
|
|
||||||
|
# On PyPy we cannot get buffers so our ability to operate here is
|
||||||
|
# serverly limited.
|
||||||
|
if pythonapi is None:
|
||||||
|
get_buffer = None
|
||||||
|
else:
|
||||||
|
|
||||||
|
def get_buffer(obj, writable=False):
|
||||||
|
buf = Py_buffer()
|
||||||
|
flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE
|
||||||
|
PyObject_GetBuffer(py_object(obj), byref(buf), flags)
|
||||||
|
try:
|
||||||
|
buffer_type = c_char * buf.len
|
||||||
|
return buffer_type.from_address(buf.buf)
|
||||||
|
finally:
|
||||||
|
PyBuffer_Release(byref(buf))
|
||||||
|
|
||||||
|
|
||||||
|
class _WindowsConsoleRawIOBase(io.RawIOBase):
|
||||||
|
def __init__(self, handle):
|
||||||
|
self.handle = handle
|
||||||
|
|
||||||
|
def isatty(self):
|
||||||
|
io.RawIOBase.isatty(self)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class _WindowsConsoleReader(_WindowsConsoleRawIOBase):
|
||||||
|
def readable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def readinto(self, b):
|
||||||
|
bytes_to_be_read = len(b)
|
||||||
|
if not bytes_to_be_read:
|
||||||
|
return 0
|
||||||
|
elif bytes_to_be_read % 2:
|
||||||
|
raise ValueError(
|
||||||
|
"cannot read odd number of bytes from UTF-16-LE encoded console"
|
||||||
|
)
|
||||||
|
|
||||||
|
buffer = get_buffer(b, writable=True)
|
||||||
|
code_units_to_be_read = bytes_to_be_read // 2
|
||||||
|
code_units_read = c_ulong()
|
||||||
|
|
||||||
|
rv = ReadConsoleW(
|
||||||
|
HANDLE(self.handle),
|
||||||
|
buffer,
|
||||||
|
code_units_to_be_read,
|
||||||
|
byref(code_units_read),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if GetLastError() == ERROR_OPERATION_ABORTED:
|
||||||
|
# wait for KeyboardInterrupt
|
||||||
|
time.sleep(0.1)
|
||||||
|
if not rv:
|
||||||
|
raise OSError("Windows error: {}".format(GetLastError()))
|
||||||
|
|
||||||
|
if buffer[0] == EOF:
|
||||||
|
return 0
|
||||||
|
return 2 * code_units_read.value
|
||||||
|
|
||||||
|
|
||||||
|
class _WindowsConsoleWriter(_WindowsConsoleRawIOBase):
|
||||||
|
def writable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_error_message(errno):
|
||||||
|
if errno == ERROR_SUCCESS:
|
||||||
|
return "ERROR_SUCCESS"
|
||||||
|
elif errno == ERROR_NOT_ENOUGH_MEMORY:
|
||||||
|
return "ERROR_NOT_ENOUGH_MEMORY"
|
||||||
|
return "Windows error {}".format(errno)
|
||||||
|
|
||||||
|
def write(self, b):
|
||||||
|
bytes_to_be_written = len(b)
|
||||||
|
buf = get_buffer(b)
|
||||||
|
code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2
|
||||||
|
code_units_written = c_ulong()
|
||||||
|
|
||||||
|
WriteConsoleW(
|
||||||
|
HANDLE(self.handle),
|
||||||
|
buf,
|
||||||
|
code_units_to_be_written,
|
||||||
|
byref(code_units_written),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
bytes_written = 2 * code_units_written.value
|
||||||
|
|
||||||
|
if bytes_written == 0 and bytes_to_be_written > 0:
|
||||||
|
raise OSError(self._get_error_message(GetLastError()))
|
||||||
|
return bytes_written
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleStream(object):
|
||||||
|
def __init__(self, text_stream, byte_stream):
|
||||||
|
self._text_stream = text_stream
|
||||||
|
self.buffer = byte_stream
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.buffer.name
|
||||||
|
|
||||||
|
def write(self, x):
|
||||||
|
if isinstance(x, text_type):
|
||||||
|
return self._text_stream.write(x)
|
||||||
|
try:
|
||||||
|
self.flush()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return self.buffer.write(x)
|
||||||
|
|
||||||
|
def writelines(self, lines):
|
||||||
|
for line in lines:
|
||||||
|
self.write(line)
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self._text_stream, name)
|
||||||
|
|
||||||
|
def isatty(self):
|
||||||
|
return self.buffer.isatty()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<ConsoleStream name={!r} encoding={!r}>".format(
|
||||||
|
self.name, self.encoding
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WindowsChunkedWriter(object):
|
||||||
|
"""
|
||||||
|
Wraps a stream (such as stdout), acting as a transparent proxy for all
|
||||||
|
attribute access apart from method 'write()' which we wrap to write in
|
||||||
|
limited chunks due to a Windows limitation on binary console streams.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, wrapped):
|
||||||
|
# double-underscore everything to prevent clashes with names of
|
||||||
|
# attributes on the wrapped stream object.
|
||||||
|
self.__wrapped = wrapped
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self.__wrapped, name)
|
||||||
|
|
||||||
|
def write(self, text):
|
||||||
|
total_to_write = len(text)
|
||||||
|
written = 0
|
||||||
|
|
||||||
|
while written < total_to_write:
|
||||||
|
to_write = min(total_to_write - written, MAX_BYTES_WRITTEN)
|
||||||
|
self.__wrapped.write(text[written : written + to_write])
|
||||||
|
written += to_write
|
||||||
|
|
||||||
|
|
||||||
|
_wrapped_std_streams = set()
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_std_stream(name):
|
||||||
|
# Python 2 & Windows 7 and below
|
||||||
|
if (
|
||||||
|
PY2
|
||||||
|
and sys.getwindowsversion()[:2] <= (6, 1)
|
||||||
|
and name not in _wrapped_std_streams
|
||||||
|
):
|
||||||
|
setattr(sys, name, WindowsChunkedWriter(getattr(sys, name)))
|
||||||
|
_wrapped_std_streams.add(name)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_text_stdin(buffer_stream):
|
||||||
|
text_stream = _NonClosingTextIOWrapper(
|
||||||
|
io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),
|
||||||
|
"utf-16-le",
|
||||||
|
"strict",
|
||||||
|
line_buffering=True,
|
||||||
|
)
|
||||||
|
return ConsoleStream(text_stream, buffer_stream)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_text_stdout(buffer_stream):
|
||||||
|
text_stream = _NonClosingTextIOWrapper(
|
||||||
|
io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)),
|
||||||
|
"utf-16-le",
|
||||||
|
"strict",
|
||||||
|
line_buffering=True,
|
||||||
|
)
|
||||||
|
return ConsoleStream(text_stream, buffer_stream)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_text_stderr(buffer_stream):
|
||||||
|
text_stream = _NonClosingTextIOWrapper(
|
||||||
|
io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)),
|
||||||
|
"utf-16-le",
|
||||||
|
"strict",
|
||||||
|
line_buffering=True,
|
||||||
|
)
|
||||||
|
return ConsoleStream(text_stream, buffer_stream)
|
||||||
|
|
||||||
|
|
||||||
|
if PY2:
|
||||||
|
|
||||||
|
def _hash_py_argv():
|
||||||
|
return zlib.crc32("\x00".join(sys.argv[1:]))
|
||||||
|
|
||||||
|
_initial_argv_hash = _hash_py_argv()
|
||||||
|
|
||||||
|
def _get_windows_argv():
|
||||||
|
argc = c_int(0)
|
||||||
|
argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc))
|
||||||
|
if not argv_unicode:
|
||||||
|
raise WinError()
|
||||||
|
try:
|
||||||
|
argv = [argv_unicode[i] for i in range(0, argc.value)]
|
||||||
|
finally:
|
||||||
|
LocalFree(argv_unicode)
|
||||||
|
del argv_unicode
|
||||||
|
|
||||||
|
if not hasattr(sys, "frozen"):
|
||||||
|
argv = argv[1:]
|
||||||
|
while len(argv) > 0:
|
||||||
|
arg = argv[0]
|
||||||
|
if not arg.startswith("-") or arg == "-":
|
||||||
|
break
|
||||||
|
argv = argv[1:]
|
||||||
|
if arg.startswith(("-c", "-m")):
|
||||||
|
break
|
||||||
|
|
||||||
|
return argv[1:]
|
||||||
|
|
||||||
|
|
||||||
|
_stream_factories = {
|
||||||
|
0: _get_text_stdin,
|
||||||
|
1: _get_text_stdout,
|
||||||
|
2: _get_text_stderr,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_console(f):
|
||||||
|
if not hasattr(f, "fileno"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
fileno = f.fileno()
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
handle = msvcrt.get_osfhandle(fileno)
|
||||||
|
return bool(GetConsoleMode(handle, byref(DWORD())))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_windows_console_stream(f, encoding, errors):
|
||||||
|
if (
|
||||||
|
get_buffer is not None
|
||||||
|
and encoding in ("utf-16-le", None)
|
||||||
|
and errors in ("strict", None)
|
||||||
|
and _is_console(f)
|
||||||
|
):
|
||||||
|
func = _stream_factories.get(f.fileno())
|
||||||
|
if func is not None:
|
||||||
|
if not PY2:
|
||||||
|
f = getattr(f, "buffer", None)
|
||||||
|
if f is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
# If we are on Python 2 we need to set the stream that we
|
||||||
|
# deal with to binary mode as otherwise the exercise if a
|
||||||
|
# bit moot. The same problems apply as for
|
||||||
|
# get_binary_stdin and friends from _compat.
|
||||||
|
msvcrt.setmode(f.fileno(), os.O_BINARY)
|
||||||
|
return func(f)
|
||||||
2030
mixly/tools/python/click/core.py
Normal file
333
mixly/tools/python/click/decorators.py
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
import inspect
|
||||||
|
import sys
|
||||||
|
from functools import update_wrapper
|
||||||
|
|
||||||
|
from ._compat import iteritems
|
||||||
|
from ._unicodefun import _check_for_unicode_literals
|
||||||
|
from .core import Argument
|
||||||
|
from .core import Command
|
||||||
|
from .core import Group
|
||||||
|
from .core import Option
|
||||||
|
from .globals import get_current_context
|
||||||
|
from .utils import echo
|
||||||
|
|
||||||
|
|
||||||
|
def pass_context(f):
|
||||||
|
"""Marks a callback as wanting to receive the current context
|
||||||
|
object as first argument.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def new_func(*args, **kwargs):
|
||||||
|
return f(get_current_context(), *args, **kwargs)
|
||||||
|
|
||||||
|
return update_wrapper(new_func, f)
|
||||||
|
|
||||||
|
|
||||||
|
def pass_obj(f):
|
||||||
|
"""Similar to :func:`pass_context`, but only pass the object on the
|
||||||
|
context onwards (:attr:`Context.obj`). This is useful if that object
|
||||||
|
represents the state of a nested system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def new_func(*args, **kwargs):
|
||||||
|
return f(get_current_context().obj, *args, **kwargs)
|
||||||
|
|
||||||
|
return update_wrapper(new_func, f)
|
||||||
|
|
||||||
|
|
||||||
|
def make_pass_decorator(object_type, ensure=False):
|
||||||
|
"""Given an object type this creates a decorator that will work
|
||||||
|
similar to :func:`pass_obj` but instead of passing the object of the
|
||||||
|
current context, it will find the innermost context of type
|
||||||
|
:func:`object_type`.
|
||||||
|
|
||||||
|
This generates a decorator that works roughly like this::
|
||||||
|
|
||||||
|
from functools import update_wrapper
|
||||||
|
|
||||||
|
def decorator(f):
|
||||||
|
@pass_context
|
||||||
|
def new_func(ctx, *args, **kwargs):
|
||||||
|
obj = ctx.find_object(object_type)
|
||||||
|
return ctx.invoke(f, obj, *args, **kwargs)
|
||||||
|
return update_wrapper(new_func, f)
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
:param object_type: the type of the object to pass.
|
||||||
|
:param ensure: if set to `True`, a new object will be created and
|
||||||
|
remembered on the context if it's not there yet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f):
|
||||||
|
def new_func(*args, **kwargs):
|
||||||
|
ctx = get_current_context()
|
||||||
|
if ensure:
|
||||||
|
obj = ctx.ensure_object(object_type)
|
||||||
|
else:
|
||||||
|
obj = ctx.find_object(object_type)
|
||||||
|
if obj is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Managed to invoke callback without a context"
|
||||||
|
" object of type '{}' existing".format(object_type.__name__)
|
||||||
|
)
|
||||||
|
return ctx.invoke(f, obj, *args, **kwargs)
|
||||||
|
|
||||||
|
return update_wrapper(new_func, f)
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def _make_command(f, name, attrs, cls):
|
||||||
|
if isinstance(f, Command):
|
||||||
|
raise TypeError("Attempted to convert a callback into a command twice.")
|
||||||
|
try:
|
||||||
|
params = f.__click_params__
|
||||||
|
params.reverse()
|
||||||
|
del f.__click_params__
|
||||||
|
except AttributeError:
|
||||||
|
params = []
|
||||||
|
help = attrs.get("help")
|
||||||
|
if help is None:
|
||||||
|
help = inspect.getdoc(f)
|
||||||
|
if isinstance(help, bytes):
|
||||||
|
help = help.decode("utf-8")
|
||||||
|
else:
|
||||||
|
help = inspect.cleandoc(help)
|
||||||
|
attrs["help"] = help
|
||||||
|
_check_for_unicode_literals()
|
||||||
|
return cls(
|
||||||
|
name=name or f.__name__.lower().replace("_", "-"),
|
||||||
|
callback=f,
|
||||||
|
params=params,
|
||||||
|
**attrs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def command(name=None, cls=None, **attrs):
|
||||||
|
r"""Creates a new :class:`Command` and uses the decorated function as
|
||||||
|
callback. This will also automatically attach all decorated
|
||||||
|
:func:`option`\s and :func:`argument`\s as parameters to the command.
|
||||||
|
|
||||||
|
The name of the command defaults to the name of the function with
|
||||||
|
underscores replaced by dashes. If you want to change that, you can
|
||||||
|
pass the intended name as the first argument.
|
||||||
|
|
||||||
|
All keyword arguments are forwarded to the underlying command class.
|
||||||
|
|
||||||
|
Once decorated the function turns into a :class:`Command` instance
|
||||||
|
that can be invoked as a command line utility or be attached to a
|
||||||
|
command :class:`Group`.
|
||||||
|
|
||||||
|
:param name: the name of the command. This defaults to the function
|
||||||
|
name with underscores replaced by dashes.
|
||||||
|
:param cls: the command class to instantiate. This defaults to
|
||||||
|
:class:`Command`.
|
||||||
|
"""
|
||||||
|
if cls is None:
|
||||||
|
cls = Command
|
||||||
|
|
||||||
|
def decorator(f):
|
||||||
|
cmd = _make_command(f, name, attrs, cls)
|
||||||
|
cmd.__doc__ = f.__doc__
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def group(name=None, **attrs):
|
||||||
|
"""Creates a new :class:`Group` with a function as callback. This
|
||||||
|
works otherwise the same as :func:`command` just that the `cls`
|
||||||
|
parameter is set to :class:`Group`.
|
||||||
|
"""
|
||||||
|
attrs.setdefault("cls", Group)
|
||||||
|
return command(name, **attrs)
|
||||||
|
|
||||||
|
|
||||||
|
def _param_memo(f, param):
|
||||||
|
if isinstance(f, Command):
|
||||||
|
f.params.append(param)
|
||||||
|
else:
|
||||||
|
if not hasattr(f, "__click_params__"):
|
||||||
|
f.__click_params__ = []
|
||||||
|
f.__click_params__.append(param)
|
||||||
|
|
||||||
|
|
||||||
|
def argument(*param_decls, **attrs):
|
||||||
|
"""Attaches an argument to the command. All positional arguments are
|
||||||
|
passed as parameter declarations to :class:`Argument`; all keyword
|
||||||
|
arguments are forwarded unchanged (except ``cls``).
|
||||||
|
This is equivalent to creating an :class:`Argument` instance manually
|
||||||
|
and attaching it to the :attr:`Command.params` list.
|
||||||
|
|
||||||
|
:param cls: the argument class to instantiate. This defaults to
|
||||||
|
:class:`Argument`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f):
|
||||||
|
ArgumentClass = attrs.pop("cls", Argument)
|
||||||
|
_param_memo(f, ArgumentClass(param_decls, **attrs))
|
||||||
|
return f
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def option(*param_decls, **attrs):
|
||||||
|
"""Attaches an option to the command. All positional arguments are
|
||||||
|
passed as parameter declarations to :class:`Option`; all keyword
|
||||||
|
arguments are forwarded unchanged (except ``cls``).
|
||||||
|
This is equivalent to creating an :class:`Option` instance manually
|
||||||
|
and attaching it to the :attr:`Command.params` list.
|
||||||
|
|
||||||
|
:param cls: the option class to instantiate. This defaults to
|
||||||
|
:class:`Option`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f):
|
||||||
|
# Issue 926, copy attrs, so pre-defined options can re-use the same cls=
|
||||||
|
option_attrs = attrs.copy()
|
||||||
|
|
||||||
|
if "help" in option_attrs:
|
||||||
|
option_attrs["help"] = inspect.cleandoc(option_attrs["help"])
|
||||||
|
OptionClass = option_attrs.pop("cls", Option)
|
||||||
|
_param_memo(f, OptionClass(param_decls, **option_attrs))
|
||||||
|
return f
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def confirmation_option(*param_decls, **attrs):
|
||||||
|
"""Shortcut for confirmation prompts that can be ignored by passing
|
||||||
|
``--yes`` as parameter.
|
||||||
|
|
||||||
|
This is equivalent to decorating a function with :func:`option` with
|
||||||
|
the following parameters::
|
||||||
|
|
||||||
|
def callback(ctx, param, value):
|
||||||
|
if not value:
|
||||||
|
ctx.abort()
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option('--yes', is_flag=True, callback=callback,
|
||||||
|
expose_value=False, prompt='Do you want to continue?')
|
||||||
|
def dropdb():
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f):
|
||||||
|
def callback(ctx, param, value):
|
||||||
|
if not value:
|
||||||
|
ctx.abort()
|
||||||
|
|
||||||
|
attrs.setdefault("is_flag", True)
|
||||||
|
attrs.setdefault("callback", callback)
|
||||||
|
attrs.setdefault("expose_value", False)
|
||||||
|
attrs.setdefault("prompt", "Do you want to continue?")
|
||||||
|
attrs.setdefault("help", "Confirm the action without prompting.")
|
||||||
|
return option(*(param_decls or ("--yes",)), **attrs)(f)
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def password_option(*param_decls, **attrs):
|
||||||
|
"""Shortcut for password prompts.
|
||||||
|
|
||||||
|
This is equivalent to decorating a function with :func:`option` with
|
||||||
|
the following parameters::
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option('--password', prompt=True, confirmation_prompt=True,
|
||||||
|
hide_input=True)
|
||||||
|
def changeadmin(password):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f):
|
||||||
|
attrs.setdefault("prompt", True)
|
||||||
|
attrs.setdefault("confirmation_prompt", True)
|
||||||
|
attrs.setdefault("hide_input", True)
|
||||||
|
return option(*(param_decls or ("--password",)), **attrs)(f)
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def version_option(version=None, *param_decls, **attrs):
|
||||||
|
"""Adds a ``--version`` option which immediately ends the program
|
||||||
|
printing out the version number. This is implemented as an eager
|
||||||
|
option that prints the version and exits the program in the callback.
|
||||||
|
|
||||||
|
:param version: the version number to show. If not provided Click
|
||||||
|
attempts an auto discovery via setuptools.
|
||||||
|
:param prog_name: the name of the program (defaults to autodetection)
|
||||||
|
:param message: custom message to show instead of the default
|
||||||
|
(``'%(prog)s, version %(version)s'``)
|
||||||
|
:param others: everything else is forwarded to :func:`option`.
|
||||||
|
"""
|
||||||
|
if version is None:
|
||||||
|
if hasattr(sys, "_getframe"):
|
||||||
|
module = sys._getframe(1).f_globals.get("__name__")
|
||||||
|
else:
|
||||||
|
module = ""
|
||||||
|
|
||||||
|
def decorator(f):
|
||||||
|
prog_name = attrs.pop("prog_name", None)
|
||||||
|
message = attrs.pop("message", "%(prog)s, version %(version)s")
|
||||||
|
|
||||||
|
def callback(ctx, param, value):
|
||||||
|
if not value or ctx.resilient_parsing:
|
||||||
|
return
|
||||||
|
prog = prog_name
|
||||||
|
if prog is None:
|
||||||
|
prog = ctx.find_root().info_name
|
||||||
|
ver = version
|
||||||
|
if ver is None:
|
||||||
|
try:
|
||||||
|
import pkg_resources
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
for dist in pkg_resources.working_set:
|
||||||
|
scripts = dist.get_entry_map().get("console_scripts") or {}
|
||||||
|
for _, entry_point in iteritems(scripts):
|
||||||
|
if entry_point.module_name == module:
|
||||||
|
ver = dist.version
|
||||||
|
break
|
||||||
|
if ver is None:
|
||||||
|
raise RuntimeError("Could not determine version")
|
||||||
|
echo(message % {"prog": prog, "version": ver}, color=ctx.color)
|
||||||
|
ctx.exit()
|
||||||
|
|
||||||
|
attrs.setdefault("is_flag", True)
|
||||||
|
attrs.setdefault("expose_value", False)
|
||||||
|
attrs.setdefault("is_eager", True)
|
||||||
|
attrs.setdefault("help", "Show the version and exit.")
|
||||||
|
attrs["callback"] = callback
|
||||||
|
return option(*(param_decls or ("--version",)), **attrs)(f)
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def help_option(*param_decls, **attrs):
|
||||||
|
"""Adds a ``--help`` option which immediately ends the program
|
||||||
|
printing out the help page. This is usually unnecessary to add as
|
||||||
|
this is added by default to all commands unless suppressed.
|
||||||
|
|
||||||
|
Like :func:`version_option`, this is implemented as eager option that
|
||||||
|
prints in the callback and exits.
|
||||||
|
|
||||||
|
All arguments are forwarded to :func:`option`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(f):
|
||||||
|
def callback(ctx, param, value):
|
||||||
|
if value and not ctx.resilient_parsing:
|
||||||
|
echo(ctx.get_help(), color=ctx.color)
|
||||||
|
ctx.exit()
|
||||||
|
|
||||||
|
attrs.setdefault("is_flag", True)
|
||||||
|
attrs.setdefault("expose_value", False)
|
||||||
|
attrs.setdefault("help", "Show this message and exit.")
|
||||||
|
attrs.setdefault("is_eager", True)
|
||||||
|
attrs["callback"] = callback
|
||||||
|
return option(*(param_decls or ("--help",)), **attrs)(f)
|
||||||
|
|
||||||
|
return decorator
|
||||||
253
mixly/tools/python/click/exceptions.py
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
from ._compat import filename_to_ui
|
||||||
|
from ._compat import get_text_stderr
|
||||||
|
from ._compat import PY2
|
||||||
|
from .utils import echo
|
||||||
|
|
||||||
|
|
||||||
|
def _join_param_hints(param_hint):
|
||||||
|
if isinstance(param_hint, (tuple, list)):
|
||||||
|
return " / ".join(repr(x) for x in param_hint)
|
||||||
|
return param_hint
|
||||||
|
|
||||||
|
|
||||||
|
class ClickException(Exception):
|
||||||
|
"""An exception that Click can handle and show to the user."""
|
||||||
|
|
||||||
|
#: The exit code for this exception
|
||||||
|
exit_code = 1
|
||||||
|
|
||||||
|
def __init__(self, message):
|
||||||
|
ctor_msg = message
|
||||||
|
if PY2:
|
||||||
|
if ctor_msg is not None:
|
||||||
|
ctor_msg = ctor_msg.encode("utf-8")
|
||||||
|
Exception.__init__(self, ctor_msg)
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
def format_message(self):
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
if PY2:
|
||||||
|
__unicode__ = __str__
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.message.encode("utf-8")
|
||||||
|
|
||||||
|
def show(self, file=None):
|
||||||
|
if file is None:
|
||||||
|
file = get_text_stderr()
|
||||||
|
echo("Error: {}".format(self.format_message()), file=file)
|
||||||
|
|
||||||
|
|
||||||
|
class UsageError(ClickException):
|
||||||
|
"""An internal exception that signals a usage error. This typically
|
||||||
|
aborts any further handling.
|
||||||
|
|
||||||
|
:param message: the error message to display.
|
||||||
|
:param ctx: optionally the context that caused this error. Click will
|
||||||
|
fill in the context automatically in some situations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
exit_code = 2
|
||||||
|
|
||||||
|
def __init__(self, message, ctx=None):
|
||||||
|
ClickException.__init__(self, message)
|
||||||
|
self.ctx = ctx
|
||||||
|
self.cmd = self.ctx.command if self.ctx else None
|
||||||
|
|
||||||
|
def show(self, file=None):
|
||||||
|
if file is None:
|
||||||
|
file = get_text_stderr()
|
||||||
|
color = None
|
||||||
|
hint = ""
|
||||||
|
if self.cmd is not None and self.cmd.get_help_option(self.ctx) is not None:
|
||||||
|
hint = "Try '{} {}' for help.\n".format(
|
||||||
|
self.ctx.command_path, self.ctx.help_option_names[0]
|
||||||
|
)
|
||||||
|
if self.ctx is not None:
|
||||||
|
color = self.ctx.color
|
||||||
|
echo("{}\n{}".format(self.ctx.get_usage(), hint), file=file, color=color)
|
||||||
|
echo("Error: {}".format(self.format_message()), file=file, color=color)
|
||||||
|
|
||||||
|
|
||||||
|
class BadParameter(UsageError):
|
||||||
|
"""An exception that formats out a standardized error message for a
|
||||||
|
bad parameter. This is useful when thrown from a callback or type as
|
||||||
|
Click will attach contextual information to it (for instance, which
|
||||||
|
parameter it is).
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
:param param: the parameter object that caused this error. This can
|
||||||
|
be left out, and Click will attach this info itself
|
||||||
|
if possible.
|
||||||
|
:param param_hint: a string that shows up as parameter name. This
|
||||||
|
can be used as alternative to `param` in cases
|
||||||
|
where custom validation should happen. If it is
|
||||||
|
a string it's used as such, if it's a list then
|
||||||
|
each item is quoted and separated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, message, ctx=None, param=None, param_hint=None):
|
||||||
|
UsageError.__init__(self, message, ctx)
|
||||||
|
self.param = param
|
||||||
|
self.param_hint = param_hint
|
||||||
|
|
||||||
|
def format_message(self):
|
||||||
|
if self.param_hint is not None:
|
||||||
|
param_hint = self.param_hint
|
||||||
|
elif self.param is not None:
|
||||||
|
param_hint = self.param.get_error_hint(self.ctx)
|
||||||
|
else:
|
||||||
|
return "Invalid value: {}".format(self.message)
|
||||||
|
param_hint = _join_param_hints(param_hint)
|
||||||
|
|
||||||
|
return "Invalid value for {}: {}".format(param_hint, self.message)
|
||||||
|
|
||||||
|
|
||||||
|
class MissingParameter(BadParameter):
|
||||||
|
"""Raised if click required an option or argument but it was not
|
||||||
|
provided when invoking the script.
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
:param param_type: a string that indicates the type of the parameter.
|
||||||
|
The default is to inherit the parameter type from
|
||||||
|
the given `param`. Valid values are ``'parameter'``,
|
||||||
|
``'option'`` or ``'argument'``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, message=None, ctx=None, param=None, param_hint=None, param_type=None
|
||||||
|
):
|
||||||
|
BadParameter.__init__(self, message, ctx, param, param_hint)
|
||||||
|
self.param_type = param_type
|
||||||
|
|
||||||
|
def format_message(self):
|
||||||
|
if self.param_hint is not None:
|
||||||
|
param_hint = self.param_hint
|
||||||
|
elif self.param is not None:
|
||||||
|
param_hint = self.param.get_error_hint(self.ctx)
|
||||||
|
else:
|
||||||
|
param_hint = None
|
||||||
|
param_hint = _join_param_hints(param_hint)
|
||||||
|
|
||||||
|
param_type = self.param_type
|
||||||
|
if param_type is None and self.param is not None:
|
||||||
|
param_type = self.param.param_type_name
|
||||||
|
|
||||||
|
msg = self.message
|
||||||
|
if self.param is not None:
|
||||||
|
msg_extra = self.param.type.get_missing_message(self.param)
|
||||||
|
if msg_extra:
|
||||||
|
if msg:
|
||||||
|
msg += ". {}".format(msg_extra)
|
||||||
|
else:
|
||||||
|
msg = msg_extra
|
||||||
|
|
||||||
|
return "Missing {}{}{}{}".format(
|
||||||
|
param_type,
|
||||||
|
" {}".format(param_hint) if param_hint else "",
|
||||||
|
". " if msg else ".",
|
||||||
|
msg or "",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.message is None:
|
||||||
|
param_name = self.param.name if self.param else None
|
||||||
|
return "missing parameter: {}".format(param_name)
|
||||||
|
else:
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
if PY2:
|
||||||
|
__unicode__ = __str__
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.__unicode__().encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
class NoSuchOption(UsageError):
|
||||||
|
"""Raised if click attempted to handle an option that does not
|
||||||
|
exist.
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, option_name, message=None, possibilities=None, ctx=None):
|
||||||
|
if message is None:
|
||||||
|
message = "no such option: {}".format(option_name)
|
||||||
|
UsageError.__init__(self, message, ctx)
|
||||||
|
self.option_name = option_name
|
||||||
|
self.possibilities = possibilities
|
||||||
|
|
||||||
|
def format_message(self):
|
||||||
|
bits = [self.message]
|
||||||
|
if self.possibilities:
|
||||||
|
if len(self.possibilities) == 1:
|
||||||
|
bits.append("Did you mean {}?".format(self.possibilities[0]))
|
||||||
|
else:
|
||||||
|
possibilities = sorted(self.possibilities)
|
||||||
|
bits.append("(Possible options: {})".format(", ".join(possibilities)))
|
||||||
|
return " ".join(bits)
|
||||||
|
|
||||||
|
|
||||||
|
class BadOptionUsage(UsageError):
|
||||||
|
"""Raised if an option is generally supplied but the use of the option
|
||||||
|
was incorrect. This is for instance raised if the number of arguments
|
||||||
|
for an option is not correct.
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
|
||||||
|
:param option_name: the name of the option being used incorrectly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, option_name, message, ctx=None):
|
||||||
|
UsageError.__init__(self, message, ctx)
|
||||||
|
self.option_name = option_name
|
||||||
|
|
||||||
|
|
||||||
|
class BadArgumentUsage(UsageError):
|
||||||
|
"""Raised if an argument is generally supplied but the use of the argument
|
||||||
|
was incorrect. This is for instance raised if the number of values
|
||||||
|
for an argument is not correct.
|
||||||
|
|
||||||
|
.. versionadded:: 6.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, message, ctx=None):
|
||||||
|
UsageError.__init__(self, message, ctx)
|
||||||
|
|
||||||
|
|
||||||
|
class FileError(ClickException):
|
||||||
|
"""Raised if a file cannot be opened."""
|
||||||
|
|
||||||
|
def __init__(self, filename, hint=None):
|
||||||
|
ui_filename = filename_to_ui(filename)
|
||||||
|
if hint is None:
|
||||||
|
hint = "unknown error"
|
||||||
|
ClickException.__init__(self, hint)
|
||||||
|
self.ui_filename = ui_filename
|
||||||
|
self.filename = filename
|
||||||
|
|
||||||
|
def format_message(self):
|
||||||
|
return "Could not open file {}: {}".format(self.ui_filename, self.message)
|
||||||
|
|
||||||
|
|
||||||
|
class Abort(RuntimeError):
|
||||||
|
"""An internal signalling exception that signals Click to abort."""
|
||||||
|
|
||||||
|
|
||||||
|
class Exit(RuntimeError):
|
||||||
|
"""An exception that indicates that the application should exit with some
|
||||||
|
status code.
|
||||||
|
|
||||||
|
:param code: the status code to exit with.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("exit_code",)
|
||||||
|
|
||||||
|
def __init__(self, code=0):
|
||||||
|
self.exit_code = code
|
||||||
283
mixly/tools/python/click/formatting.py
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from ._compat import term_len
|
||||||
|
from .parser import split_opt
|
||||||
|
from .termui import get_terminal_size
|
||||||
|
|
||||||
|
# Can force a width. This is used by the test system
|
||||||
|
FORCED_WIDTH = None
|
||||||
|
|
||||||
|
|
||||||
|
def measure_table(rows):
|
||||||
|
widths = {}
|
||||||
|
for row in rows:
|
||||||
|
for idx, col in enumerate(row):
|
||||||
|
widths[idx] = max(widths.get(idx, 0), term_len(col))
|
||||||
|
return tuple(y for x, y in sorted(widths.items()))
|
||||||
|
|
||||||
|
|
||||||
|
def iter_rows(rows, col_count):
|
||||||
|
for row in rows:
|
||||||
|
row = tuple(row)
|
||||||
|
yield row + ("",) * (col_count - len(row))
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_text(
|
||||||
|
text, width=78, initial_indent="", subsequent_indent="", preserve_paragraphs=False
|
||||||
|
):
|
||||||
|
"""A helper function that intelligently wraps text. By default, it
|
||||||
|
assumes that it operates on a single paragraph of text but if the
|
||||||
|
`preserve_paragraphs` parameter is provided it will intelligently
|
||||||
|
handle paragraphs (defined by two empty lines).
|
||||||
|
|
||||||
|
If paragraphs are handled, a paragraph can be prefixed with an empty
|
||||||
|
line containing the ``\\b`` character (``\\x08``) to indicate that
|
||||||
|
no rewrapping should happen in that block.
|
||||||
|
|
||||||
|
:param text: the text that should be rewrapped.
|
||||||
|
:param width: the maximum width for the text.
|
||||||
|
:param initial_indent: the initial indent that should be placed on the
|
||||||
|
first line as a string.
|
||||||
|
:param subsequent_indent: the indent string that should be placed on
|
||||||
|
each consecutive line.
|
||||||
|
:param preserve_paragraphs: if this flag is set then the wrapping will
|
||||||
|
intelligently handle paragraphs.
|
||||||
|
"""
|
||||||
|
from ._textwrap import TextWrapper
|
||||||
|
|
||||||
|
text = text.expandtabs()
|
||||||
|
wrapper = TextWrapper(
|
||||||
|
width,
|
||||||
|
initial_indent=initial_indent,
|
||||||
|
subsequent_indent=subsequent_indent,
|
||||||
|
replace_whitespace=False,
|
||||||
|
)
|
||||||
|
if not preserve_paragraphs:
|
||||||
|
return wrapper.fill(text)
|
||||||
|
|
||||||
|
p = []
|
||||||
|
buf = []
|
||||||
|
indent = None
|
||||||
|
|
||||||
|
def _flush_par():
|
||||||
|
if not buf:
|
||||||
|
return
|
||||||
|
if buf[0].strip() == "\b":
|
||||||
|
p.append((indent or 0, True, "\n".join(buf[1:])))
|
||||||
|
else:
|
||||||
|
p.append((indent or 0, False, " ".join(buf)))
|
||||||
|
del buf[:]
|
||||||
|
|
||||||
|
for line in text.splitlines():
|
||||||
|
if not line:
|
||||||
|
_flush_par()
|
||||||
|
indent = None
|
||||||
|
else:
|
||||||
|
if indent is None:
|
||||||
|
orig_len = term_len(line)
|
||||||
|
line = line.lstrip()
|
||||||
|
indent = orig_len - term_len(line)
|
||||||
|
buf.append(line)
|
||||||
|
_flush_par()
|
||||||
|
|
||||||
|
rv = []
|
||||||
|
for indent, raw, text in p:
|
||||||
|
with wrapper.extra_indent(" " * indent):
|
||||||
|
if raw:
|
||||||
|
rv.append(wrapper.indent_only(text))
|
||||||
|
else:
|
||||||
|
rv.append(wrapper.fill(text))
|
||||||
|
|
||||||
|
return "\n\n".join(rv)
|
||||||
|
|
||||||
|
|
||||||
|
class HelpFormatter(object):
|
||||||
|
"""This class helps with formatting text-based help pages. It's
|
||||||
|
usually just needed for very special internal cases, but it's also
|
||||||
|
exposed so that developers can write their own fancy outputs.
|
||||||
|
|
||||||
|
At present, it always writes into memory.
|
||||||
|
|
||||||
|
:param indent_increment: the additional increment for each level.
|
||||||
|
:param width: the width for the text. This defaults to the terminal
|
||||||
|
width clamped to a maximum of 78.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, indent_increment=2, width=None, max_width=None):
|
||||||
|
self.indent_increment = indent_increment
|
||||||
|
if max_width is None:
|
||||||
|
max_width = 80
|
||||||
|
if width is None:
|
||||||
|
width = FORCED_WIDTH
|
||||||
|
if width is None:
|
||||||
|
width = max(min(get_terminal_size()[0], max_width) - 2, 50)
|
||||||
|
self.width = width
|
||||||
|
self.current_indent = 0
|
||||||
|
self.buffer = []
|
||||||
|
|
||||||
|
def write(self, string):
|
||||||
|
"""Writes a unicode string into the internal buffer."""
|
||||||
|
self.buffer.append(string)
|
||||||
|
|
||||||
|
def indent(self):
|
||||||
|
"""Increases the indentation."""
|
||||||
|
self.current_indent += self.indent_increment
|
||||||
|
|
||||||
|
def dedent(self):
|
||||||
|
"""Decreases the indentation."""
|
||||||
|
self.current_indent -= self.indent_increment
|
||||||
|
|
||||||
|
def write_usage(self, prog, args="", prefix="Usage: "):
|
||||||
|
"""Writes a usage line into the buffer.
|
||||||
|
|
||||||
|
:param prog: the program name.
|
||||||
|
:param args: whitespace separated list of arguments.
|
||||||
|
:param prefix: the prefix for the first line.
|
||||||
|
"""
|
||||||
|
usage_prefix = "{:>{w}}{} ".format(prefix, prog, w=self.current_indent)
|
||||||
|
text_width = self.width - self.current_indent
|
||||||
|
|
||||||
|
if text_width >= (term_len(usage_prefix) + 20):
|
||||||
|
# The arguments will fit to the right of the prefix.
|
||||||
|
indent = " " * term_len(usage_prefix)
|
||||||
|
self.write(
|
||||||
|
wrap_text(
|
||||||
|
args,
|
||||||
|
text_width,
|
||||||
|
initial_indent=usage_prefix,
|
||||||
|
subsequent_indent=indent,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# The prefix is too long, put the arguments on the next line.
|
||||||
|
self.write(usage_prefix)
|
||||||
|
self.write("\n")
|
||||||
|
indent = " " * (max(self.current_indent, term_len(prefix)) + 4)
|
||||||
|
self.write(
|
||||||
|
wrap_text(
|
||||||
|
args, text_width, initial_indent=indent, subsequent_indent=indent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.write("\n")
|
||||||
|
|
||||||
|
def write_heading(self, heading):
|
||||||
|
"""Writes a heading into the buffer."""
|
||||||
|
self.write("{:>{w}}{}:\n".format("", heading, w=self.current_indent))
|
||||||
|
|
||||||
|
def write_paragraph(self):
|
||||||
|
"""Writes a paragraph into the buffer."""
|
||||||
|
if self.buffer:
|
||||||
|
self.write("\n")
|
||||||
|
|
||||||
|
def write_text(self, text):
|
||||||
|
"""Writes re-indented text into the buffer. This rewraps and
|
||||||
|
preserves paragraphs.
|
||||||
|
"""
|
||||||
|
text_width = max(self.width - self.current_indent, 11)
|
||||||
|
indent = " " * self.current_indent
|
||||||
|
self.write(
|
||||||
|
wrap_text(
|
||||||
|
text,
|
||||||
|
text_width,
|
||||||
|
initial_indent=indent,
|
||||||
|
subsequent_indent=indent,
|
||||||
|
preserve_paragraphs=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.write("\n")
|
||||||
|
|
||||||
|
def write_dl(self, rows, col_max=30, col_spacing=2):
|
||||||
|
"""Writes a definition list into the buffer. This is how options
|
||||||
|
and commands are usually formatted.
|
||||||
|
|
||||||
|
:param rows: a list of two item tuples for the terms and values.
|
||||||
|
:param col_max: the maximum width of the first column.
|
||||||
|
:param col_spacing: the number of spaces between the first and
|
||||||
|
second column.
|
||||||
|
"""
|
||||||
|
rows = list(rows)
|
||||||
|
widths = measure_table(rows)
|
||||||
|
if len(widths) != 2:
|
||||||
|
raise TypeError("Expected two columns for definition list")
|
||||||
|
|
||||||
|
first_col = min(widths[0], col_max) + col_spacing
|
||||||
|
|
||||||
|
for first, second in iter_rows(rows, len(widths)):
|
||||||
|
self.write("{:>{w}}{}".format("", first, w=self.current_indent))
|
||||||
|
if not second:
|
||||||
|
self.write("\n")
|
||||||
|
continue
|
||||||
|
if term_len(first) <= first_col - col_spacing:
|
||||||
|
self.write(" " * (first_col - term_len(first)))
|
||||||
|
else:
|
||||||
|
self.write("\n")
|
||||||
|
self.write(" " * (first_col + self.current_indent))
|
||||||
|
|
||||||
|
text_width = max(self.width - first_col - 2, 10)
|
||||||
|
wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True)
|
||||||
|
lines = wrapped_text.splitlines()
|
||||||
|
|
||||||
|
if lines:
|
||||||
|
self.write("{}\n".format(lines[0]))
|
||||||
|
|
||||||
|
for line in lines[1:]:
|
||||||
|
self.write(
|
||||||
|
"{:>{w}}{}\n".format(
|
||||||
|
"", line, w=first_col + self.current_indent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(lines) > 1:
|
||||||
|
# separate long help from next option
|
||||||
|
self.write("\n")
|
||||||
|
else:
|
||||||
|
self.write("\n")
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def section(self, name):
|
||||||
|
"""Helpful context manager that writes a paragraph, a heading,
|
||||||
|
and the indents.
|
||||||
|
|
||||||
|
:param name: the section name that is written as heading.
|
||||||
|
"""
|
||||||
|
self.write_paragraph()
|
||||||
|
self.write_heading(name)
|
||||||
|
self.indent()
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self.dedent()
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def indentation(self):
|
||||||
|
"""A context manager that increases the indentation."""
|
||||||
|
self.indent()
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self.dedent()
|
||||||
|
|
||||||
|
def getvalue(self):
|
||||||
|
"""Returns the buffer contents."""
|
||||||
|
return "".join(self.buffer)
|
||||||
|
|
||||||
|
|
||||||
|
def join_options(options):
|
||||||
|
"""Given a list of option strings this joins them in the most appropriate
|
||||||
|
way and returns them in the form ``(formatted_string,
|
||||||
|
any_prefix_is_slash)`` where the second item in the tuple is a flag that
|
||||||
|
indicates if any of the option prefixes was a slash.
|
||||||
|
"""
|
||||||
|
rv = []
|
||||||
|
any_prefix_is_slash = False
|
||||||
|
for opt in options:
|
||||||
|
prefix = split_opt(opt)[0]
|
||||||
|
if prefix == "/":
|
||||||
|
any_prefix_is_slash = True
|
||||||
|
rv.append((len(prefix), opt))
|
||||||
|
|
||||||
|
rv.sort(key=lambda x: x[0])
|
||||||
|
|
||||||
|
rv = ", ".join(x[1] for x in rv)
|
||||||
|
return rv, any_prefix_is_slash
|
||||||
47
mixly/tools/python/click/globals.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from threading import local
|
||||||
|
|
||||||
|
_local = local()
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_context(silent=False):
|
||||||
|
"""Returns the current click context. This can be used as a way to
|
||||||
|
access the current context object from anywhere. This is a more implicit
|
||||||
|
alternative to the :func:`pass_context` decorator. This function is
|
||||||
|
primarily useful for helpers such as :func:`echo` which might be
|
||||||
|
interested in changing its behavior based on the current context.
|
||||||
|
|
||||||
|
To push the current context, :meth:`Context.scope` can be used.
|
||||||
|
|
||||||
|
.. versionadded:: 5.0
|
||||||
|
|
||||||
|
:param silent: if set to `True` the return value is `None` if no context
|
||||||
|
is available. The default behavior is to raise a
|
||||||
|
:exc:`RuntimeError`.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return _local.stack[-1]
|
||||||
|
except (AttributeError, IndexError):
|
||||||
|
if not silent:
|
||||||
|
raise RuntimeError("There is no active click context.")
|
||||||
|
|
||||||
|
|
||||||
|
def push_context(ctx):
|
||||||
|
"""Pushes a new context to the current stack."""
|
||||||
|
_local.__dict__.setdefault("stack", []).append(ctx)
|
||||||
|
|
||||||
|
|
||||||
|
def pop_context():
|
||||||
|
"""Removes the top level from the stack."""
|
||||||
|
_local.stack.pop()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_color_default(color=None):
|
||||||
|
""""Internal helper to get the default value of the color flag. If a
|
||||||
|
value is passed it's returned unchanged, otherwise it's looked up from
|
||||||
|
the current context.
|
||||||
|
"""
|
||||||
|
if color is not None:
|
||||||
|
return color
|
||||||
|
ctx = get_current_context(silent=True)
|
||||||
|
if ctx is not None:
|
||||||
|
return ctx.color
|
||||||
428
mixly/tools/python/click/parser.py
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This module started out as largely a copy paste from the stdlib's
|
||||||
|
optparse module with the features removed that we do not need from
|
||||||
|
optparse because we implement them in Click on a higher level (for
|
||||||
|
instance type handling, help formatting and a lot more).
|
||||||
|
|
||||||
|
The plan is to remove more and more from here over time.
|
||||||
|
|
||||||
|
The reason this is a different module and not optparse from the stdlib
|
||||||
|
is that there are differences in 2.x and 3.x about the error messages
|
||||||
|
generated and optparse in the stdlib uses gettext for no good reason
|
||||||
|
and might cause us issues.
|
||||||
|
|
||||||
|
Click uses parts of optparse written by Gregory P. Ward and maintained
|
||||||
|
by the Python Software Foundation. This is limited to code in parser.py.
|
||||||
|
|
||||||
|
Copyright 2001-2006 Gregory P. Ward. All rights reserved.
|
||||||
|
Copyright 2002-2006 Python Software Foundation. All rights reserved.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
from .exceptions import BadArgumentUsage
|
||||||
|
from .exceptions import BadOptionUsage
|
||||||
|
from .exceptions import NoSuchOption
|
||||||
|
from .exceptions import UsageError
|
||||||
|
|
||||||
|
|
||||||
|
def _unpack_args(args, nargs_spec):
|
||||||
|
"""Given an iterable of arguments and an iterable of nargs specifications,
|
||||||
|
it returns a tuple with all the unpacked arguments at the first index
|
||||||
|
and all remaining arguments as the second.
|
||||||
|
|
||||||
|
The nargs specification is the number of arguments that should be consumed
|
||||||
|
or `-1` to indicate that this position should eat up all the remainders.
|
||||||
|
|
||||||
|
Missing items are filled with `None`.
|
||||||
|
"""
|
||||||
|
args = deque(args)
|
||||||
|
nargs_spec = deque(nargs_spec)
|
||||||
|
rv = []
|
||||||
|
spos = None
|
||||||
|
|
||||||
|
def _fetch(c):
|
||||||
|
try:
|
||||||
|
if spos is None:
|
||||||
|
return c.popleft()
|
||||||
|
else:
|
||||||
|
return c.pop()
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
while nargs_spec:
|
||||||
|
nargs = _fetch(nargs_spec)
|
||||||
|
if nargs == 1:
|
||||||
|
rv.append(_fetch(args))
|
||||||
|
elif nargs > 1:
|
||||||
|
x = [_fetch(args) for _ in range(nargs)]
|
||||||
|
# If we're reversed, we're pulling in the arguments in reverse,
|
||||||
|
# so we need to turn them around.
|
||||||
|
if spos is not None:
|
||||||
|
x.reverse()
|
||||||
|
rv.append(tuple(x))
|
||||||
|
elif nargs < 0:
|
||||||
|
if spos is not None:
|
||||||
|
raise TypeError("Cannot have two nargs < 0")
|
||||||
|
spos = len(rv)
|
||||||
|
rv.append(None)
|
||||||
|
|
||||||
|
# spos is the position of the wildcard (star). If it's not `None`,
|
||||||
|
# we fill it with the remainder.
|
||||||
|
if spos is not None:
|
||||||
|
rv[spos] = tuple(args)
|
||||||
|
args = []
|
||||||
|
rv[spos + 1 :] = reversed(rv[spos + 1 :])
|
||||||
|
|
||||||
|
return tuple(rv), list(args)
|
||||||
|
|
||||||
|
|
||||||
|
def _error_opt_args(nargs, opt):
|
||||||
|
if nargs == 1:
|
||||||
|
raise BadOptionUsage(opt, "{} option requires an argument".format(opt))
|
||||||
|
raise BadOptionUsage(opt, "{} option requires {} arguments".format(opt, nargs))
|
||||||
|
|
||||||
|
|
||||||
|
def split_opt(opt):
|
||||||
|
first = opt[:1]
|
||||||
|
if first.isalnum():
|
||||||
|
return "", opt
|
||||||
|
if opt[1:2] == first:
|
||||||
|
return opt[:2], opt[2:]
|
||||||
|
return first, opt[1:]
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_opt(opt, ctx):
|
||||||
|
if ctx is None or ctx.token_normalize_func is None:
|
||||||
|
return opt
|
||||||
|
prefix, opt = split_opt(opt)
|
||||||
|
return prefix + ctx.token_normalize_func(opt)
|
||||||
|
|
||||||
|
|
||||||
|
def split_arg_string(string):
|
||||||
|
"""Given an argument string this attempts to split it into small parts."""
|
||||||
|
rv = []
|
||||||
|
for match in re.finditer(
|
||||||
|
r"('([^'\\]*(?:\\.[^'\\]*)*)'|\"([^\"\\]*(?:\\.[^\"\\]*)*)\"|\S+)\s*",
|
||||||
|
string,
|
||||||
|
re.S,
|
||||||
|
):
|
||||||
|
arg = match.group().strip()
|
||||||
|
if arg[:1] == arg[-1:] and arg[:1] in "\"'":
|
||||||
|
arg = arg[1:-1].encode("ascii", "backslashreplace").decode("unicode-escape")
|
||||||
|
try:
|
||||||
|
arg = type(string)(arg)
|
||||||
|
except UnicodeError:
|
||||||
|
pass
|
||||||
|
rv.append(arg)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
class Option(object):
|
||||||
|
def __init__(self, opts, dest, action=None, nargs=1, const=None, obj=None):
|
||||||
|
self._short_opts = []
|
||||||
|
self._long_opts = []
|
||||||
|
self.prefixes = set()
|
||||||
|
|
||||||
|
for opt in opts:
|
||||||
|
prefix, value = split_opt(opt)
|
||||||
|
if not prefix:
|
||||||
|
raise ValueError("Invalid start character for option ({})".format(opt))
|
||||||
|
self.prefixes.add(prefix[0])
|
||||||
|
if len(prefix) == 1 and len(value) == 1:
|
||||||
|
self._short_opts.append(opt)
|
||||||
|
else:
|
||||||
|
self._long_opts.append(opt)
|
||||||
|
self.prefixes.add(prefix)
|
||||||
|
|
||||||
|
if action is None:
|
||||||
|
action = "store"
|
||||||
|
|
||||||
|
self.dest = dest
|
||||||
|
self.action = action
|
||||||
|
self.nargs = nargs
|
||||||
|
self.const = const
|
||||||
|
self.obj = obj
|
||||||
|
|
||||||
|
@property
|
||||||
|
def takes_value(self):
|
||||||
|
return self.action in ("store", "append")
|
||||||
|
|
||||||
|
def process(self, value, state):
|
||||||
|
if self.action == "store":
|
||||||
|
state.opts[self.dest] = value
|
||||||
|
elif self.action == "store_const":
|
||||||
|
state.opts[self.dest] = self.const
|
||||||
|
elif self.action == "append":
|
||||||
|
state.opts.setdefault(self.dest, []).append(value)
|
||||||
|
elif self.action == "append_const":
|
||||||
|
state.opts.setdefault(self.dest, []).append(self.const)
|
||||||
|
elif self.action == "count":
|
||||||
|
state.opts[self.dest] = state.opts.get(self.dest, 0) + 1
|
||||||
|
else:
|
||||||
|
raise ValueError("unknown action '{}'".format(self.action))
|
||||||
|
state.order.append(self.obj)
|
||||||
|
|
||||||
|
|
||||||
|
class Argument(object):
|
||||||
|
def __init__(self, dest, nargs=1, obj=None):
|
||||||
|
self.dest = dest
|
||||||
|
self.nargs = nargs
|
||||||
|
self.obj = obj
|
||||||
|
|
||||||
|
def process(self, value, state):
|
||||||
|
if self.nargs > 1:
|
||||||
|
holes = sum(1 for x in value if x is None)
|
||||||
|
if holes == len(value):
|
||||||
|
value = None
|
||||||
|
elif holes != 0:
|
||||||
|
raise BadArgumentUsage(
|
||||||
|
"argument {} takes {} values".format(self.dest, self.nargs)
|
||||||
|
)
|
||||||
|
state.opts[self.dest] = value
|
||||||
|
state.order.append(self.obj)
|
||||||
|
|
||||||
|
|
||||||
|
class ParsingState(object):
|
||||||
|
def __init__(self, rargs):
|
||||||
|
self.opts = {}
|
||||||
|
self.largs = []
|
||||||
|
self.rargs = rargs
|
||||||
|
self.order = []
|
||||||
|
|
||||||
|
|
||||||
|
class OptionParser(object):
|
||||||
|
"""The option parser is an internal class that is ultimately used to
|
||||||
|
parse options and arguments. It's modelled after optparse and brings
|
||||||
|
a similar but vastly simplified API. It should generally not be used
|
||||||
|
directly as the high level Click classes wrap it for you.
|
||||||
|
|
||||||
|
It's not nearly as extensible as optparse or argparse as it does not
|
||||||
|
implement features that are implemented on a higher level (such as
|
||||||
|
types or defaults).
|
||||||
|
|
||||||
|
:param ctx: optionally the :class:`~click.Context` where this parser
|
||||||
|
should go with.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ctx=None):
|
||||||
|
#: The :class:`~click.Context` for this parser. This might be
|
||||||
|
#: `None` for some advanced use cases.
|
||||||
|
self.ctx = ctx
|
||||||
|
#: This controls how the parser deals with interspersed arguments.
|
||||||
|
#: If this is set to `False`, the parser will stop on the first
|
||||||
|
#: non-option. Click uses this to implement nested subcommands
|
||||||
|
#: safely.
|
||||||
|
self.allow_interspersed_args = True
|
||||||
|
#: This tells the parser how to deal with unknown options. By
|
||||||
|
#: default it will error out (which is sensible), but there is a
|
||||||
|
#: second mode where it will ignore it and continue processing
|
||||||
|
#: after shifting all the unknown options into the resulting args.
|
||||||
|
self.ignore_unknown_options = False
|
||||||
|
if ctx is not None:
|
||||||
|
self.allow_interspersed_args = ctx.allow_interspersed_args
|
||||||
|
self.ignore_unknown_options = ctx.ignore_unknown_options
|
||||||
|
self._short_opt = {}
|
||||||
|
self._long_opt = {}
|
||||||
|
self._opt_prefixes = {"-", "--"}
|
||||||
|
self._args = []
|
||||||
|
|
||||||
|
def add_option(self, opts, dest, action=None, nargs=1, const=None, obj=None):
|
||||||
|
"""Adds a new option named `dest` to the parser. The destination
|
||||||
|
is not inferred (unlike with optparse) and needs to be explicitly
|
||||||
|
provided. Action can be any of ``store``, ``store_const``,
|
||||||
|
``append``, ``appnd_const`` or ``count``.
|
||||||
|
|
||||||
|
The `obj` can be used to identify the option in the order list
|
||||||
|
that is returned from the parser.
|
||||||
|
"""
|
||||||
|
if obj is None:
|
||||||
|
obj = dest
|
||||||
|
opts = [normalize_opt(opt, self.ctx) for opt in opts]
|
||||||
|
option = Option(opts, dest, action=action, nargs=nargs, const=const, obj=obj)
|
||||||
|
self._opt_prefixes.update(option.prefixes)
|
||||||
|
for opt in option._short_opts:
|
||||||
|
self._short_opt[opt] = option
|
||||||
|
for opt in option._long_opts:
|
||||||
|
self._long_opt[opt] = option
|
||||||
|
|
||||||
|
def add_argument(self, dest, nargs=1, obj=None):
|
||||||
|
"""Adds a positional argument named `dest` to the parser.
|
||||||
|
|
||||||
|
The `obj` can be used to identify the option in the order list
|
||||||
|
that is returned from the parser.
|
||||||
|
"""
|
||||||
|
if obj is None:
|
||||||
|
obj = dest
|
||||||
|
self._args.append(Argument(dest=dest, nargs=nargs, obj=obj))
|
||||||
|
|
||||||
|
def parse_args(self, args):
|
||||||
|
"""Parses positional arguments and returns ``(values, args, order)``
|
||||||
|
for the parsed options and arguments as well as the leftover
|
||||||
|
arguments if there are any. The order is a list of objects as they
|
||||||
|
appear on the command line. If arguments appear multiple times they
|
||||||
|
will be memorized multiple times as well.
|
||||||
|
"""
|
||||||
|
state = ParsingState(args)
|
||||||
|
try:
|
||||||
|
self._process_args_for_options(state)
|
||||||
|
self._process_args_for_args(state)
|
||||||
|
except UsageError:
|
||||||
|
if self.ctx is None or not self.ctx.resilient_parsing:
|
||||||
|
raise
|
||||||
|
return state.opts, state.largs, state.order
|
||||||
|
|
||||||
|
def _process_args_for_args(self, state):
|
||||||
|
pargs, args = _unpack_args(
|
||||||
|
state.largs + state.rargs, [x.nargs for x in self._args]
|
||||||
|
)
|
||||||
|
|
||||||
|
for idx, arg in enumerate(self._args):
|
||||||
|
arg.process(pargs[idx], state)
|
||||||
|
|
||||||
|
state.largs = args
|
||||||
|
state.rargs = []
|
||||||
|
|
||||||
|
def _process_args_for_options(self, state):
|
||||||
|
while state.rargs:
|
||||||
|
arg = state.rargs.pop(0)
|
||||||
|
arglen = len(arg)
|
||||||
|
# Double dashes always handled explicitly regardless of what
|
||||||
|
# prefixes are valid.
|
||||||
|
if arg == "--":
|
||||||
|
return
|
||||||
|
elif arg[:1] in self._opt_prefixes and arglen > 1:
|
||||||
|
self._process_opts(arg, state)
|
||||||
|
elif self.allow_interspersed_args:
|
||||||
|
state.largs.append(arg)
|
||||||
|
else:
|
||||||
|
state.rargs.insert(0, arg)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Say this is the original argument list:
|
||||||
|
# [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)]
|
||||||
|
# ^
|
||||||
|
# (we are about to process arg(i)).
|
||||||
|
#
|
||||||
|
# Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of
|
||||||
|
# [arg0, ..., arg(i-1)] (any options and their arguments will have
|
||||||
|
# been removed from largs).
|
||||||
|
#
|
||||||
|
# The while loop will usually consume 1 or more arguments per pass.
|
||||||
|
# If it consumes 1 (eg. arg is an option that takes no arguments),
|
||||||
|
# then after _process_arg() is done the situation is:
|
||||||
|
#
|
||||||
|
# largs = subset of [arg0, ..., arg(i)]
|
||||||
|
# rargs = [arg(i+1), ..., arg(N-1)]
|
||||||
|
#
|
||||||
|
# If allow_interspersed_args is false, largs will always be
|
||||||
|
# *empty* -- still a subset of [arg0, ..., arg(i-1)], but
|
||||||
|
# not a very interesting subset!
|
||||||
|
|
||||||
|
def _match_long_opt(self, opt, explicit_value, state):
|
||||||
|
if opt not in self._long_opt:
|
||||||
|
possibilities = [word for word in self._long_opt if word.startswith(opt)]
|
||||||
|
raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx)
|
||||||
|
|
||||||
|
option = self._long_opt[opt]
|
||||||
|
if option.takes_value:
|
||||||
|
# At this point it's safe to modify rargs by injecting the
|
||||||
|
# explicit value, because no exception is raised in this
|
||||||
|
# branch. This means that the inserted value will be fully
|
||||||
|
# consumed.
|
||||||
|
if explicit_value is not None:
|
||||||
|
state.rargs.insert(0, explicit_value)
|
||||||
|
|
||||||
|
nargs = option.nargs
|
||||||
|
if len(state.rargs) < nargs:
|
||||||
|
_error_opt_args(nargs, opt)
|
||||||
|
elif nargs == 1:
|
||||||
|
value = state.rargs.pop(0)
|
||||||
|
else:
|
||||||
|
value = tuple(state.rargs[:nargs])
|
||||||
|
del state.rargs[:nargs]
|
||||||
|
|
||||||
|
elif explicit_value is not None:
|
||||||
|
raise BadOptionUsage(opt, "{} option does not take a value".format(opt))
|
||||||
|
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
|
||||||
|
option.process(value, state)
|
||||||
|
|
||||||
|
def _match_short_opt(self, arg, state):
|
||||||
|
stop = False
|
||||||
|
i = 1
|
||||||
|
prefix = arg[0]
|
||||||
|
unknown_options = []
|
||||||
|
|
||||||
|
for ch in arg[1:]:
|
||||||
|
opt = normalize_opt(prefix + ch, self.ctx)
|
||||||
|
option = self._short_opt.get(opt)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if not option:
|
||||||
|
if self.ignore_unknown_options:
|
||||||
|
unknown_options.append(ch)
|
||||||
|
continue
|
||||||
|
raise NoSuchOption(opt, ctx=self.ctx)
|
||||||
|
if option.takes_value:
|
||||||
|
# Any characters left in arg? Pretend they're the
|
||||||
|
# next arg, and stop consuming characters of arg.
|
||||||
|
if i < len(arg):
|
||||||
|
state.rargs.insert(0, arg[i:])
|
||||||
|
stop = True
|
||||||
|
|
||||||
|
nargs = option.nargs
|
||||||
|
if len(state.rargs) < nargs:
|
||||||
|
_error_opt_args(nargs, opt)
|
||||||
|
elif nargs == 1:
|
||||||
|
value = state.rargs.pop(0)
|
||||||
|
else:
|
||||||
|
value = tuple(state.rargs[:nargs])
|
||||||
|
del state.rargs[:nargs]
|
||||||
|
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
|
||||||
|
option.process(value, state)
|
||||||
|
|
||||||
|
if stop:
|
||||||
|
break
|
||||||
|
|
||||||
|
# If we got any unknown options we re-combinate the string of the
|
||||||
|
# remaining options and re-attach the prefix, then report that
|
||||||
|
# to the state as new larg. This way there is basic combinatorics
|
||||||
|
# that can be achieved while still ignoring unknown arguments.
|
||||||
|
if self.ignore_unknown_options and unknown_options:
|
||||||
|
state.largs.append("{}{}".format(prefix, "".join(unknown_options)))
|
||||||
|
|
||||||
|
def _process_opts(self, arg, state):
|
||||||
|
explicit_value = None
|
||||||
|
# Long option handling happens in two parts. The first part is
|
||||||
|
# supporting explicitly attached values. In any case, we will try
|
||||||
|
# to long match the option first.
|
||||||
|
if "=" in arg:
|
||||||
|
long_opt, explicit_value = arg.split("=", 1)
|
||||||
|
else:
|
||||||
|
long_opt = arg
|
||||||
|
norm_long_opt = normalize_opt(long_opt, self.ctx)
|
||||||
|
|
||||||
|
# At this point we will match the (assumed) long option through
|
||||||
|
# the long option matching code. Note that this allows options
|
||||||
|
# like "-foo" to be matched as long options.
|
||||||
|
try:
|
||||||
|
self._match_long_opt(norm_long_opt, explicit_value, state)
|
||||||
|
except NoSuchOption:
|
||||||
|
# At this point the long option matching failed, and we need
|
||||||
|
# to try with short options. However there is a special rule
|
||||||
|
# which says, that if we have a two character options prefix
|
||||||
|
# (applies to "--foo" for instance), we do not dispatch to the
|
||||||
|
# short option code and will instead raise the no option
|
||||||
|
# error.
|
||||||
|
if arg[:2] not in self._opt_prefixes:
|
||||||
|
return self._match_short_opt(arg, state)
|
||||||
|
if not self.ignore_unknown_options:
|
||||||
|
raise
|
||||||
|
state.largs.append(arg)
|
||||||
681
mixly/tools/python/click/termui.py
Normal file
@@ -0,0 +1,681 @@
|
|||||||
|
import inspect
|
||||||
|
import io
|
||||||
|
import itertools
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ._compat import DEFAULT_COLUMNS
|
||||||
|
from ._compat import get_winterm_size
|
||||||
|
from ._compat import isatty
|
||||||
|
from ._compat import raw_input
|
||||||
|
from ._compat import string_types
|
||||||
|
from ._compat import strip_ansi
|
||||||
|
from ._compat import text_type
|
||||||
|
from ._compat import WIN
|
||||||
|
from .exceptions import Abort
|
||||||
|
from .exceptions import UsageError
|
||||||
|
from .globals import resolve_color_default
|
||||||
|
from .types import Choice
|
||||||
|
from .types import convert_type
|
||||||
|
from .types import Path
|
||||||
|
from .utils import echo
|
||||||
|
from .utils import LazyFile
|
||||||
|
|
||||||
|
# The prompt functions to use. The doc tools currently override these
|
||||||
|
# functions to customize how they work.
|
||||||
|
visible_prompt_func = raw_input
|
||||||
|
|
||||||
|
_ansi_colors = {
|
||||||
|
"black": 30,
|
||||||
|
"red": 31,
|
||||||
|
"green": 32,
|
||||||
|
"yellow": 33,
|
||||||
|
"blue": 34,
|
||||||
|
"magenta": 35,
|
||||||
|
"cyan": 36,
|
||||||
|
"white": 37,
|
||||||
|
"reset": 39,
|
||||||
|
"bright_black": 90,
|
||||||
|
"bright_red": 91,
|
||||||
|
"bright_green": 92,
|
||||||
|
"bright_yellow": 93,
|
||||||
|
"bright_blue": 94,
|
||||||
|
"bright_magenta": 95,
|
||||||
|
"bright_cyan": 96,
|
||||||
|
"bright_white": 97,
|
||||||
|
}
|
||||||
|
_ansi_reset_all = "\033[0m"
|
||||||
|
|
||||||
|
|
||||||
|
def hidden_prompt_func(prompt):
|
||||||
|
import getpass
|
||||||
|
|
||||||
|
return getpass.getpass(prompt)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_prompt(
|
||||||
|
text, suffix, show_default=False, default=None, show_choices=True, type=None
|
||||||
|
):
|
||||||
|
prompt = text
|
||||||
|
if type is not None and show_choices and isinstance(type, Choice):
|
||||||
|
prompt += " ({})".format(", ".join(map(str, type.choices)))
|
||||||
|
if default is not None and show_default:
|
||||||
|
prompt = "{} [{}]".format(prompt, _format_default(default))
|
||||||
|
return prompt + suffix
|
||||||
|
|
||||||
|
|
||||||
|
def _format_default(default):
|
||||||
|
if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"):
|
||||||
|
return default.name
|
||||||
|
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def prompt(
|
||||||
|
text,
|
||||||
|
default=None,
|
||||||
|
hide_input=False,
|
||||||
|
confirmation_prompt=False,
|
||||||
|
type=None,
|
||||||
|
value_proc=None,
|
||||||
|
prompt_suffix=": ",
|
||||||
|
show_default=True,
|
||||||
|
err=False,
|
||||||
|
show_choices=True,
|
||||||
|
):
|
||||||
|
"""Prompts a user for input. This is a convenience function that can
|
||||||
|
be used to prompt a user for input later.
|
||||||
|
|
||||||
|
If the user aborts the input by sending a interrupt signal, this
|
||||||
|
function will catch it and raise a :exc:`Abort` exception.
|
||||||
|
|
||||||
|
.. versionadded:: 7.0
|
||||||
|
Added the show_choices parameter.
|
||||||
|
|
||||||
|
.. versionadded:: 6.0
|
||||||
|
Added unicode support for cmd.exe on Windows.
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
Added the `err` parameter.
|
||||||
|
|
||||||
|
:param text: the text to show for the prompt.
|
||||||
|
:param default: the default value to use if no input happens. If this
|
||||||
|
is not given it will prompt until it's aborted.
|
||||||
|
:param hide_input: if this is set to true then the input value will
|
||||||
|
be hidden.
|
||||||
|
:param confirmation_prompt: asks for confirmation for the value.
|
||||||
|
:param type: the type to use to check the value against.
|
||||||
|
:param value_proc: if this parameter is provided it's a function that
|
||||||
|
is invoked instead of the type conversion to
|
||||||
|
convert a value.
|
||||||
|
:param prompt_suffix: a suffix that should be added to the prompt.
|
||||||
|
:param show_default: shows or hides the default value in the prompt.
|
||||||
|
:param err: if set to true the file defaults to ``stderr`` instead of
|
||||||
|
``stdout``, the same as with echo.
|
||||||
|
:param show_choices: Show or hide choices if the passed type is a Choice.
|
||||||
|
For example if type is a Choice of either day or week,
|
||||||
|
show_choices is true and text is "Group by" then the
|
||||||
|
prompt will be "Group by (day, week): ".
|
||||||
|
"""
|
||||||
|
result = None
|
||||||
|
|
||||||
|
def prompt_func(text):
|
||||||
|
f = hidden_prompt_func if hide_input else visible_prompt_func
|
||||||
|
try:
|
||||||
|
# Write the prompt separately so that we get nice
|
||||||
|
# coloring through colorama on Windows
|
||||||
|
echo(text, nl=False, err=err)
|
||||||
|
return f("")
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
# getpass doesn't print a newline if the user aborts input with ^C.
|
||||||
|
# Allegedly this behavior is inherited from getpass(3).
|
||||||
|
# A doc bug has been filed at https://bugs.python.org/issue24711
|
||||||
|
if hide_input:
|
||||||
|
echo(None, err=err)
|
||||||
|
raise Abort()
|
||||||
|
|
||||||
|
if value_proc is None:
|
||||||
|
value_proc = convert_type(type, default)
|
||||||
|
|
||||||
|
prompt = _build_prompt(
|
||||||
|
text, prompt_suffix, show_default, default, show_choices, type
|
||||||
|
)
|
||||||
|
|
||||||
|
while 1:
|
||||||
|
while 1:
|
||||||
|
value = prompt_func(prompt)
|
||||||
|
if value:
|
||||||
|
break
|
||||||
|
elif default is not None:
|
||||||
|
if isinstance(value_proc, Path):
|
||||||
|
# validate Path default value(exists, dir_okay etc.)
|
||||||
|
value = default
|
||||||
|
break
|
||||||
|
return default
|
||||||
|
try:
|
||||||
|
result = value_proc(value)
|
||||||
|
except UsageError as e:
|
||||||
|
echo("Error: {}".format(e.message), err=err) # noqa: B306
|
||||||
|
continue
|
||||||
|
if not confirmation_prompt:
|
||||||
|
return result
|
||||||
|
while 1:
|
||||||
|
value2 = prompt_func("Repeat for confirmation: ")
|
||||||
|
if value2:
|
||||||
|
break
|
||||||
|
if value == value2:
|
||||||
|
return result
|
||||||
|
echo("Error: the two entered values do not match", err=err)
|
||||||
|
|
||||||
|
|
||||||
|
def confirm(
|
||||||
|
text, default=False, abort=False, prompt_suffix=": ", show_default=True, err=False
|
||||||
|
):
|
||||||
|
"""Prompts for confirmation (yes/no question).
|
||||||
|
|
||||||
|
If the user aborts the input by sending a interrupt signal this
|
||||||
|
function will catch it and raise a :exc:`Abort` exception.
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
Added the `err` parameter.
|
||||||
|
|
||||||
|
:param text: the question to ask.
|
||||||
|
:param default: the default for the prompt.
|
||||||
|
:param abort: if this is set to `True` a negative answer aborts the
|
||||||
|
exception by raising :exc:`Abort`.
|
||||||
|
:param prompt_suffix: a suffix that should be added to the prompt.
|
||||||
|
:param show_default: shows or hides the default value in the prompt.
|
||||||
|
:param err: if set to true the file defaults to ``stderr`` instead of
|
||||||
|
``stdout``, the same as with echo.
|
||||||
|
"""
|
||||||
|
prompt = _build_prompt(
|
||||||
|
text, prompt_suffix, show_default, "Y/n" if default else "y/N"
|
||||||
|
)
|
||||||
|
while 1:
|
||||||
|
try:
|
||||||
|
# Write the prompt separately so that we get nice
|
||||||
|
# coloring through colorama on Windows
|
||||||
|
echo(prompt, nl=False, err=err)
|
||||||
|
value = visible_prompt_func("").lower().strip()
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
raise Abort()
|
||||||
|
if value in ("y", "yes"):
|
||||||
|
rv = True
|
||||||
|
elif value in ("n", "no"):
|
||||||
|
rv = False
|
||||||
|
elif value == "":
|
||||||
|
rv = default
|
||||||
|
else:
|
||||||
|
echo("Error: invalid input", err=err)
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
if abort and not rv:
|
||||||
|
raise Abort()
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
def get_terminal_size():
|
||||||
|
"""Returns the current size of the terminal as tuple in the form
|
||||||
|
``(width, height)`` in columns and rows.
|
||||||
|
"""
|
||||||
|
# If shutil has get_terminal_size() (Python 3.3 and later) use that
|
||||||
|
if sys.version_info >= (3, 3):
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
shutil_get_terminal_size = getattr(shutil, "get_terminal_size", None)
|
||||||
|
if shutil_get_terminal_size:
|
||||||
|
sz = shutil_get_terminal_size()
|
||||||
|
return sz.columns, sz.lines
|
||||||
|
|
||||||
|
# We provide a sensible default for get_winterm_size() when being invoked
|
||||||
|
# inside a subprocess. Without this, it would not provide a useful input.
|
||||||
|
if get_winterm_size is not None:
|
||||||
|
size = get_winterm_size()
|
||||||
|
if size == (0, 0):
|
||||||
|
return (79, 24)
|
||||||
|
else:
|
||||||
|
return size
|
||||||
|
|
||||||
|
def ioctl_gwinsz(fd):
|
||||||
|
try:
|
||||||
|
import fcntl
|
||||||
|
import termios
|
||||||
|
|
||||||
|
cr = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234"))
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
return cr
|
||||||
|
|
||||||
|
cr = ioctl_gwinsz(0) or ioctl_gwinsz(1) or ioctl_gwinsz(2)
|
||||||
|
if not cr:
|
||||||
|
try:
|
||||||
|
fd = os.open(os.ctermid(), os.O_RDONLY)
|
||||||
|
try:
|
||||||
|
cr = ioctl_gwinsz(fd)
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if not cr or not cr[0] or not cr[1]:
|
||||||
|
cr = (os.environ.get("LINES", 25), os.environ.get("COLUMNS", DEFAULT_COLUMNS))
|
||||||
|
return int(cr[1]), int(cr[0])
|
||||||
|
|
||||||
|
|
||||||
|
def echo_via_pager(text_or_generator, color=None):
|
||||||
|
"""This function takes a text and shows it via an environment specific
|
||||||
|
pager on stdout.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
Added the `color` flag.
|
||||||
|
|
||||||
|
:param text_or_generator: the text to page, or alternatively, a
|
||||||
|
generator emitting the text to page.
|
||||||
|
:param color: controls if the pager supports ANSI colors or not. The
|
||||||
|
default is autodetection.
|
||||||
|
"""
|
||||||
|
color = resolve_color_default(color)
|
||||||
|
|
||||||
|
if inspect.isgeneratorfunction(text_or_generator):
|
||||||
|
i = text_or_generator()
|
||||||
|
elif isinstance(text_or_generator, string_types):
|
||||||
|
i = [text_or_generator]
|
||||||
|
else:
|
||||||
|
i = iter(text_or_generator)
|
||||||
|
|
||||||
|
# convert every element of i to a text type if necessary
|
||||||
|
text_generator = (el if isinstance(el, string_types) else text_type(el) for el in i)
|
||||||
|
|
||||||
|
from ._termui_impl import pager
|
||||||
|
|
||||||
|
return pager(itertools.chain(text_generator, "\n"), color)
|
||||||
|
|
||||||
|
|
||||||
|
def progressbar(
|
||||||
|
iterable=None,
|
||||||
|
length=None,
|
||||||
|
label=None,
|
||||||
|
show_eta=True,
|
||||||
|
show_percent=None,
|
||||||
|
show_pos=False,
|
||||||
|
item_show_func=None,
|
||||||
|
fill_char="#",
|
||||||
|
empty_char="-",
|
||||||
|
bar_template="%(label)s [%(bar)s] %(info)s",
|
||||||
|
info_sep=" ",
|
||||||
|
width=36,
|
||||||
|
file=None,
|
||||||
|
color=None,
|
||||||
|
):
|
||||||
|
"""This function creates an iterable context manager that can be used
|
||||||
|
to iterate over something while showing a progress bar. It will
|
||||||
|
either iterate over the `iterable` or `length` items (that are counted
|
||||||
|
up). While iteration happens, this function will print a rendered
|
||||||
|
progress bar to the given `file` (defaults to stdout) and will attempt
|
||||||
|
to calculate remaining time and more. By default, this progress bar
|
||||||
|
will not be rendered if the file is not a terminal.
|
||||||
|
|
||||||
|
The context manager creates the progress bar. When the context
|
||||||
|
manager is entered the progress bar is already created. With every
|
||||||
|
iteration over the progress bar, the iterable passed to the bar is
|
||||||
|
advanced and the bar is updated. When the context manager exits,
|
||||||
|
a newline is printed and the progress bar is finalized on screen.
|
||||||
|
|
||||||
|
Note: The progress bar is currently designed for use cases where the
|
||||||
|
total progress can be expected to take at least several seconds.
|
||||||
|
Because of this, the ProgressBar class object won't display
|
||||||
|
progress that is considered too fast, and progress where the time
|
||||||
|
between steps is less than a second.
|
||||||
|
|
||||||
|
No printing must happen or the progress bar will be unintentionally
|
||||||
|
destroyed.
|
||||||
|
|
||||||
|
Example usage::
|
||||||
|
|
||||||
|
with progressbar(items) as bar:
|
||||||
|
for item in bar:
|
||||||
|
do_something_with(item)
|
||||||
|
|
||||||
|
Alternatively, if no iterable is specified, one can manually update the
|
||||||
|
progress bar through the `update()` method instead of directly
|
||||||
|
iterating over the progress bar. The update method accepts the number
|
||||||
|
of steps to increment the bar with::
|
||||||
|
|
||||||
|
with progressbar(length=chunks.total_bytes) as bar:
|
||||||
|
for chunk in chunks:
|
||||||
|
process_chunk(chunk)
|
||||||
|
bar.update(chunks.bytes)
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
Added the `color` parameter. Added a `update` method to the
|
||||||
|
progressbar object.
|
||||||
|
|
||||||
|
:param iterable: an iterable to iterate over. If not provided the length
|
||||||
|
is required.
|
||||||
|
:param length: the number of items to iterate over. By default the
|
||||||
|
progressbar will attempt to ask the iterator about its
|
||||||
|
length, which might or might not work. If an iterable is
|
||||||
|
also provided this parameter can be used to override the
|
||||||
|
length. If an iterable is not provided the progress bar
|
||||||
|
will iterate over a range of that length.
|
||||||
|
:param label: the label to show next to the progress bar.
|
||||||
|
:param show_eta: enables or disables the estimated time display. This is
|
||||||
|
automatically disabled if the length cannot be
|
||||||
|
determined.
|
||||||
|
:param show_percent: enables or disables the percentage display. The
|
||||||
|
default is `True` if the iterable has a length or
|
||||||
|
`False` if not.
|
||||||
|
:param show_pos: enables or disables the absolute position display. The
|
||||||
|
default is `False`.
|
||||||
|
:param item_show_func: a function called with the current item which
|
||||||
|
can return a string to show the current item
|
||||||
|
next to the progress bar. Note that the current
|
||||||
|
item can be `None`!
|
||||||
|
:param fill_char: the character to use to show the filled part of the
|
||||||
|
progress bar.
|
||||||
|
:param empty_char: the character to use to show the non-filled part of
|
||||||
|
the progress bar.
|
||||||
|
:param bar_template: the format string to use as template for the bar.
|
||||||
|
The parameters in it are ``label`` for the label,
|
||||||
|
``bar`` for the progress bar and ``info`` for the
|
||||||
|
info section.
|
||||||
|
:param info_sep: the separator between multiple info items (eta etc.)
|
||||||
|
:param width: the width of the progress bar in characters, 0 means full
|
||||||
|
terminal width
|
||||||
|
:param file: the file to write to. If this is not a terminal then
|
||||||
|
only the label is printed.
|
||||||
|
:param color: controls if the terminal supports ANSI colors or not. The
|
||||||
|
default is autodetection. This is only needed if ANSI
|
||||||
|
codes are included anywhere in the progress bar output
|
||||||
|
which is not the case by default.
|
||||||
|
"""
|
||||||
|
from ._termui_impl import ProgressBar
|
||||||
|
|
||||||
|
color = resolve_color_default(color)
|
||||||
|
return ProgressBar(
|
||||||
|
iterable=iterable,
|
||||||
|
length=length,
|
||||||
|
show_eta=show_eta,
|
||||||
|
show_percent=show_percent,
|
||||||
|
show_pos=show_pos,
|
||||||
|
item_show_func=item_show_func,
|
||||||
|
fill_char=fill_char,
|
||||||
|
empty_char=empty_char,
|
||||||
|
bar_template=bar_template,
|
||||||
|
info_sep=info_sep,
|
||||||
|
file=file,
|
||||||
|
label=label,
|
||||||
|
width=width,
|
||||||
|
color=color,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def clear():
|
||||||
|
"""Clears the terminal screen. This will have the effect of clearing
|
||||||
|
the whole visible space of the terminal and moving the cursor to the
|
||||||
|
top left. This does not do anything if not connected to a terminal.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
if not isatty(sys.stdout):
|
||||||
|
return
|
||||||
|
# If we're on Windows and we don't have colorama available, then we
|
||||||
|
# clear the screen by shelling out. Otherwise we can use an escape
|
||||||
|
# sequence.
|
||||||
|
if WIN:
|
||||||
|
os.system("cls")
|
||||||
|
else:
|
||||||
|
sys.stdout.write("\033[2J\033[1;1H")
|
||||||
|
|
||||||
|
|
||||||
|
def style(
|
||||||
|
text,
|
||||||
|
fg=None,
|
||||||
|
bg=None,
|
||||||
|
bold=None,
|
||||||
|
dim=None,
|
||||||
|
underline=None,
|
||||||
|
blink=None,
|
||||||
|
reverse=None,
|
||||||
|
reset=True,
|
||||||
|
):
|
||||||
|
"""Styles a text with ANSI styles and returns the new string. By
|
||||||
|
default the styling is self contained which means that at the end
|
||||||
|
of the string a reset code is issued. This can be prevented by
|
||||||
|
passing ``reset=False``.
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
click.echo(click.style('Hello World!', fg='green'))
|
||||||
|
click.echo(click.style('ATTENTION!', blink=True))
|
||||||
|
click.echo(click.style('Some things', reverse=True, fg='cyan'))
|
||||||
|
|
||||||
|
Supported color names:
|
||||||
|
|
||||||
|
* ``black`` (might be a gray)
|
||||||
|
* ``red``
|
||||||
|
* ``green``
|
||||||
|
* ``yellow`` (might be an orange)
|
||||||
|
* ``blue``
|
||||||
|
* ``magenta``
|
||||||
|
* ``cyan``
|
||||||
|
* ``white`` (might be light gray)
|
||||||
|
* ``bright_black``
|
||||||
|
* ``bright_red``
|
||||||
|
* ``bright_green``
|
||||||
|
* ``bright_yellow``
|
||||||
|
* ``bright_blue``
|
||||||
|
* ``bright_magenta``
|
||||||
|
* ``bright_cyan``
|
||||||
|
* ``bright_white``
|
||||||
|
* ``reset`` (reset the color code only)
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
.. versionadded:: 7.0
|
||||||
|
Added support for bright colors.
|
||||||
|
|
||||||
|
:param text: the string to style with ansi codes.
|
||||||
|
:param fg: if provided this will become the foreground color.
|
||||||
|
:param bg: if provided this will become the background color.
|
||||||
|
:param bold: if provided this will enable or disable bold mode.
|
||||||
|
:param dim: if provided this will enable or disable dim mode. This is
|
||||||
|
badly supported.
|
||||||
|
:param underline: if provided this will enable or disable underline.
|
||||||
|
:param blink: if provided this will enable or disable blinking.
|
||||||
|
:param reverse: if provided this will enable or disable inverse
|
||||||
|
rendering (foreground becomes background and the
|
||||||
|
other way round).
|
||||||
|
:param reset: by default a reset-all code is added at the end of the
|
||||||
|
string which means that styles do not carry over. This
|
||||||
|
can be disabled to compose styles.
|
||||||
|
"""
|
||||||
|
bits = []
|
||||||
|
if fg:
|
||||||
|
try:
|
||||||
|
bits.append("\033[{}m".format(_ansi_colors[fg]))
|
||||||
|
except KeyError:
|
||||||
|
raise TypeError("Unknown color '{}'".format(fg))
|
||||||
|
if bg:
|
||||||
|
try:
|
||||||
|
bits.append("\033[{}m".format(_ansi_colors[bg] + 10))
|
||||||
|
except KeyError:
|
||||||
|
raise TypeError("Unknown color '{}'".format(bg))
|
||||||
|
if bold is not None:
|
||||||
|
bits.append("\033[{}m".format(1 if bold else 22))
|
||||||
|
if dim is not None:
|
||||||
|
bits.append("\033[{}m".format(2 if dim else 22))
|
||||||
|
if underline is not None:
|
||||||
|
bits.append("\033[{}m".format(4 if underline else 24))
|
||||||
|
if blink is not None:
|
||||||
|
bits.append("\033[{}m".format(5 if blink else 25))
|
||||||
|
if reverse is not None:
|
||||||
|
bits.append("\033[{}m".format(7 if reverse else 27))
|
||||||
|
bits.append(text)
|
||||||
|
if reset:
|
||||||
|
bits.append(_ansi_reset_all)
|
||||||
|
return "".join(bits)
|
||||||
|
|
||||||
|
|
||||||
|
def unstyle(text):
|
||||||
|
"""Removes ANSI styling information from a string. Usually it's not
|
||||||
|
necessary to use this function as Click's echo function will
|
||||||
|
automatically remove styling if necessary.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
:param text: the text to remove style information from.
|
||||||
|
"""
|
||||||
|
return strip_ansi(text)
|
||||||
|
|
||||||
|
|
||||||
|
def secho(message=None, file=None, nl=True, err=False, color=None, **styles):
|
||||||
|
"""This function combines :func:`echo` and :func:`style` into one
|
||||||
|
call. As such the following two calls are the same::
|
||||||
|
|
||||||
|
click.secho('Hello World!', fg='green')
|
||||||
|
click.echo(click.style('Hello World!', fg='green'))
|
||||||
|
|
||||||
|
All keyword arguments are forwarded to the underlying functions
|
||||||
|
depending on which one they go with.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
if message is not None:
|
||||||
|
message = style(message, **styles)
|
||||||
|
return echo(message, file=file, nl=nl, err=err, color=color)
|
||||||
|
|
||||||
|
|
||||||
|
def edit(
|
||||||
|
text=None, editor=None, env=None, require_save=True, extension=".txt", filename=None
|
||||||
|
):
|
||||||
|
r"""Edits the given text in the defined editor. If an editor is given
|
||||||
|
(should be the full path to the executable but the regular operating
|
||||||
|
system search path is used for finding the executable) it overrides
|
||||||
|
the detected editor. Optionally, some environment variables can be
|
||||||
|
used. If the editor is closed without changes, `None` is returned. In
|
||||||
|
case a file is edited directly the return value is always `None` and
|
||||||
|
`require_save` and `extension` are ignored.
|
||||||
|
|
||||||
|
If the editor cannot be opened a :exc:`UsageError` is raised.
|
||||||
|
|
||||||
|
Note for Windows: to simplify cross-platform usage, the newlines are
|
||||||
|
automatically converted from POSIX to Windows and vice versa. As such,
|
||||||
|
the message here will have ``\n`` as newline markers.
|
||||||
|
|
||||||
|
:param text: the text to edit.
|
||||||
|
:param editor: optionally the editor to use. Defaults to automatic
|
||||||
|
detection.
|
||||||
|
:param env: environment variables to forward to the editor.
|
||||||
|
:param require_save: if this is true, then not saving in the editor
|
||||||
|
will make the return value become `None`.
|
||||||
|
:param extension: the extension to tell the editor about. This defaults
|
||||||
|
to `.txt` but changing this might change syntax
|
||||||
|
highlighting.
|
||||||
|
:param filename: if provided it will edit this file instead of the
|
||||||
|
provided text contents. It will not use a temporary
|
||||||
|
file as an indirection in that case.
|
||||||
|
"""
|
||||||
|
from ._termui_impl import Editor
|
||||||
|
|
||||||
|
editor = Editor(
|
||||||
|
editor=editor, env=env, require_save=require_save, extension=extension
|
||||||
|
)
|
||||||
|
if filename is None:
|
||||||
|
return editor.edit(text)
|
||||||
|
editor.edit_file(filename)
|
||||||
|
|
||||||
|
|
||||||
|
def launch(url, wait=False, locate=False):
|
||||||
|
"""This function launches the given URL (or filename) in the default
|
||||||
|
viewer application for this file type. If this is an executable, it
|
||||||
|
might launch the executable in a new session. The return value is
|
||||||
|
the exit code of the launched application. Usually, ``0`` indicates
|
||||||
|
success.
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
click.launch('https://click.palletsprojects.com/')
|
||||||
|
click.launch('/my/downloaded/file', locate=True)
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
:param url: URL or filename of the thing to launch.
|
||||||
|
:param wait: waits for the program to stop.
|
||||||
|
:param locate: if this is set to `True` then instead of launching the
|
||||||
|
application associated with the URL it will attempt to
|
||||||
|
launch a file manager with the file located. This
|
||||||
|
might have weird effects if the URL does not point to
|
||||||
|
the filesystem.
|
||||||
|
"""
|
||||||
|
from ._termui_impl import open_url
|
||||||
|
|
||||||
|
return open_url(url, wait=wait, locate=locate)
|
||||||
|
|
||||||
|
|
||||||
|
# If this is provided, getchar() calls into this instead. This is used
|
||||||
|
# for unittesting purposes.
|
||||||
|
_getchar = None
|
||||||
|
|
||||||
|
|
||||||
|
def getchar(echo=False):
|
||||||
|
"""Fetches a single character from the terminal and returns it. This
|
||||||
|
will always return a unicode character and under certain rare
|
||||||
|
circumstances this might return more than one character. The
|
||||||
|
situations which more than one character is returned is when for
|
||||||
|
whatever reason multiple characters end up in the terminal buffer or
|
||||||
|
standard input was not actually a terminal.
|
||||||
|
|
||||||
|
Note that this will always read from the terminal, even if something
|
||||||
|
is piped into the standard input.
|
||||||
|
|
||||||
|
Note for Windows: in rare cases when typing non-ASCII characters, this
|
||||||
|
function might wait for a second character and then return both at once.
|
||||||
|
This is because certain Unicode characters look like special-key markers.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
:param echo: if set to `True`, the character read will also show up on
|
||||||
|
the terminal. The default is to not show it.
|
||||||
|
"""
|
||||||
|
f = _getchar
|
||||||
|
if f is None:
|
||||||
|
from ._termui_impl import getchar as f
|
||||||
|
return f(echo)
|
||||||
|
|
||||||
|
|
||||||
|
def raw_terminal():
|
||||||
|
from ._termui_impl import raw_terminal as f
|
||||||
|
|
||||||
|
return f()
|
||||||
|
|
||||||
|
|
||||||
|
def pause(info="Press any key to continue ...", err=False):
|
||||||
|
"""This command stops execution and waits for the user to press any
|
||||||
|
key to continue. This is similar to the Windows batch "pause"
|
||||||
|
command. If the program is not run through a terminal, this command
|
||||||
|
will instead do nothing.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
Added the `err` parameter.
|
||||||
|
|
||||||
|
:param info: the info string to print before pausing.
|
||||||
|
:param err: if set to message goes to ``stderr`` instead of
|
||||||
|
``stdout``, the same as with echo.
|
||||||
|
"""
|
||||||
|
if not isatty(sys.stdin) or not isatty(sys.stdout):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if info:
|
||||||
|
echo(info, nl=False, err=err)
|
||||||
|
try:
|
||||||
|
getchar()
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if info:
|
||||||
|
echo(err=err)
|
||||||
382
mixly/tools/python/click/testing.py
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
import contextlib
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from . import formatting
|
||||||
|
from . import termui
|
||||||
|
from . import utils
|
||||||
|
from ._compat import iteritems
|
||||||
|
from ._compat import PY2
|
||||||
|
from ._compat import string_types
|
||||||
|
|
||||||
|
|
||||||
|
if PY2:
|
||||||
|
from cStringIO import StringIO
|
||||||
|
else:
|
||||||
|
import io
|
||||||
|
from ._compat import _find_binary_reader
|
||||||
|
|
||||||
|
|
||||||
|
class EchoingStdin(object):
|
||||||
|
def __init__(self, input, output):
|
||||||
|
self._input = input
|
||||||
|
self._output = output
|
||||||
|
|
||||||
|
def __getattr__(self, x):
|
||||||
|
return getattr(self._input, x)
|
||||||
|
|
||||||
|
def _echo(self, rv):
|
||||||
|
self._output.write(rv)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def read(self, n=-1):
|
||||||
|
return self._echo(self._input.read(n))
|
||||||
|
|
||||||
|
def readline(self, n=-1):
|
||||||
|
return self._echo(self._input.readline(n))
|
||||||
|
|
||||||
|
def readlines(self):
|
||||||
|
return [self._echo(x) for x in self._input.readlines()]
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._echo(x) for x in self._input)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return repr(self._input)
|
||||||
|
|
||||||
|
|
||||||
|
def make_input_stream(input, charset):
|
||||||
|
# Is already an input stream.
|
||||||
|
if hasattr(input, "read"):
|
||||||
|
if PY2:
|
||||||
|
return input
|
||||||
|
rv = _find_binary_reader(input)
|
||||||
|
if rv is not None:
|
||||||
|
return rv
|
||||||
|
raise TypeError("Could not find binary reader for input stream.")
|
||||||
|
|
||||||
|
if input is None:
|
||||||
|
input = b""
|
||||||
|
elif not isinstance(input, bytes):
|
||||||
|
input = input.encode(charset)
|
||||||
|
if PY2:
|
||||||
|
return StringIO(input)
|
||||||
|
return io.BytesIO(input)
|
||||||
|
|
||||||
|
|
||||||
|
class Result(object):
|
||||||
|
"""Holds the captured result of an invoked CLI script."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, runner, stdout_bytes, stderr_bytes, exit_code, exception, exc_info=None
|
||||||
|
):
|
||||||
|
#: The runner that created the result
|
||||||
|
self.runner = runner
|
||||||
|
#: The standard output as bytes.
|
||||||
|
self.stdout_bytes = stdout_bytes
|
||||||
|
#: The standard error as bytes, or None if not available
|
||||||
|
self.stderr_bytes = stderr_bytes
|
||||||
|
#: The exit code as integer.
|
||||||
|
self.exit_code = exit_code
|
||||||
|
#: The exception that happened if one did.
|
||||||
|
self.exception = exception
|
||||||
|
#: The traceback
|
||||||
|
self.exc_info = exc_info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def output(self):
|
||||||
|
"""The (standard) output as unicode string."""
|
||||||
|
return self.stdout
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stdout(self):
|
||||||
|
"""The standard output as unicode string."""
|
||||||
|
return self.stdout_bytes.decode(self.runner.charset, "replace").replace(
|
||||||
|
"\r\n", "\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stderr(self):
|
||||||
|
"""The standard error as unicode string."""
|
||||||
|
if self.stderr_bytes is None:
|
||||||
|
raise ValueError("stderr not separately captured")
|
||||||
|
return self.stderr_bytes.decode(self.runner.charset, "replace").replace(
|
||||||
|
"\r\n", "\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<{} {}>".format(
|
||||||
|
type(self).__name__, repr(self.exception) if self.exception else "okay"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CliRunner(object):
|
||||||
|
"""The CLI runner provides functionality to invoke a Click command line
|
||||||
|
script for unittesting purposes in a isolated environment. This only
|
||||||
|
works in single-threaded systems without any concurrency as it changes the
|
||||||
|
global interpreter state.
|
||||||
|
|
||||||
|
:param charset: the character set for the input and output data. This is
|
||||||
|
UTF-8 by default and should not be changed currently as
|
||||||
|
the reporting to Click only works in Python 2 properly.
|
||||||
|
:param env: a dictionary with environment variables for overriding.
|
||||||
|
:param echo_stdin: if this is set to `True`, then reading from stdin writes
|
||||||
|
to stdout. This is useful for showing examples in
|
||||||
|
some circumstances. Note that regular prompts
|
||||||
|
will automatically echo the input.
|
||||||
|
:param mix_stderr: if this is set to `False`, then stdout and stderr are
|
||||||
|
preserved as independent streams. This is useful for
|
||||||
|
Unix-philosophy apps that have predictable stdout and
|
||||||
|
noisy stderr, such that each may be measured
|
||||||
|
independently
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, charset=None, env=None, echo_stdin=False, mix_stderr=True):
|
||||||
|
if charset is None:
|
||||||
|
charset = "utf-8"
|
||||||
|
self.charset = charset
|
||||||
|
self.env = env or {}
|
||||||
|
self.echo_stdin = echo_stdin
|
||||||
|
self.mix_stderr = mix_stderr
|
||||||
|
|
||||||
|
def get_default_prog_name(self, cli):
|
||||||
|
"""Given a command object it will return the default program name
|
||||||
|
for it. The default is the `name` attribute or ``"root"`` if not
|
||||||
|
set.
|
||||||
|
"""
|
||||||
|
return cli.name or "root"
|
||||||
|
|
||||||
|
def make_env(self, overrides=None):
|
||||||
|
"""Returns the environment overrides for invoking a script."""
|
||||||
|
rv = dict(self.env)
|
||||||
|
if overrides:
|
||||||
|
rv.update(overrides)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def isolation(self, input=None, env=None, color=False):
|
||||||
|
"""A context manager that sets up the isolation for invoking of a
|
||||||
|
command line tool. This sets up stdin with the given input data
|
||||||
|
and `os.environ` with the overrides from the given dictionary.
|
||||||
|
This also rebinds some internals in Click to be mocked (like the
|
||||||
|
prompt functionality).
|
||||||
|
|
||||||
|
This is automatically done in the :meth:`invoke` method.
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
The ``color`` parameter was added.
|
||||||
|
|
||||||
|
:param input: the input stream to put into sys.stdin.
|
||||||
|
:param env: the environment overrides as dictionary.
|
||||||
|
:param color: whether the output should contain color codes. The
|
||||||
|
application can still override this explicitly.
|
||||||
|
"""
|
||||||
|
input = make_input_stream(input, self.charset)
|
||||||
|
|
||||||
|
old_stdin = sys.stdin
|
||||||
|
old_stdout = sys.stdout
|
||||||
|
old_stderr = sys.stderr
|
||||||
|
old_forced_width = formatting.FORCED_WIDTH
|
||||||
|
formatting.FORCED_WIDTH = 80
|
||||||
|
|
||||||
|
env = self.make_env(env)
|
||||||
|
|
||||||
|
if PY2:
|
||||||
|
bytes_output = StringIO()
|
||||||
|
if self.echo_stdin:
|
||||||
|
input = EchoingStdin(input, bytes_output)
|
||||||
|
sys.stdout = bytes_output
|
||||||
|
if not self.mix_stderr:
|
||||||
|
bytes_error = StringIO()
|
||||||
|
sys.stderr = bytes_error
|
||||||
|
else:
|
||||||
|
bytes_output = io.BytesIO()
|
||||||
|
if self.echo_stdin:
|
||||||
|
input = EchoingStdin(input, bytes_output)
|
||||||
|
input = io.TextIOWrapper(input, encoding=self.charset)
|
||||||
|
sys.stdout = io.TextIOWrapper(bytes_output, encoding=self.charset)
|
||||||
|
if not self.mix_stderr:
|
||||||
|
bytes_error = io.BytesIO()
|
||||||
|
sys.stderr = io.TextIOWrapper(bytes_error, encoding=self.charset)
|
||||||
|
|
||||||
|
if self.mix_stderr:
|
||||||
|
sys.stderr = sys.stdout
|
||||||
|
|
||||||
|
sys.stdin = input
|
||||||
|
|
||||||
|
def visible_input(prompt=None):
|
||||||
|
sys.stdout.write(prompt or "")
|
||||||
|
val = input.readline().rstrip("\r\n")
|
||||||
|
sys.stdout.write("{}\n".format(val))
|
||||||
|
sys.stdout.flush()
|
||||||
|
return val
|
||||||
|
|
||||||
|
def hidden_input(prompt=None):
|
||||||
|
sys.stdout.write("{}\n".format(prompt or ""))
|
||||||
|
sys.stdout.flush()
|
||||||
|
return input.readline().rstrip("\r\n")
|
||||||
|
|
||||||
|
def _getchar(echo):
|
||||||
|
char = sys.stdin.read(1)
|
||||||
|
if echo:
|
||||||
|
sys.stdout.write(char)
|
||||||
|
sys.stdout.flush()
|
||||||
|
return char
|
||||||
|
|
||||||
|
default_color = color
|
||||||
|
|
||||||
|
def should_strip_ansi(stream=None, color=None):
|
||||||
|
if color is None:
|
||||||
|
return not default_color
|
||||||
|
return not color
|
||||||
|
|
||||||
|
old_visible_prompt_func = termui.visible_prompt_func
|
||||||
|
old_hidden_prompt_func = termui.hidden_prompt_func
|
||||||
|
old__getchar_func = termui._getchar
|
||||||
|
old_should_strip_ansi = utils.should_strip_ansi
|
||||||
|
termui.visible_prompt_func = visible_input
|
||||||
|
termui.hidden_prompt_func = hidden_input
|
||||||
|
termui._getchar = _getchar
|
||||||
|
utils.should_strip_ansi = should_strip_ansi
|
||||||
|
|
||||||
|
old_env = {}
|
||||||
|
try:
|
||||||
|
for key, value in iteritems(env):
|
||||||
|
old_env[key] = os.environ.get(key)
|
||||||
|
if value is None:
|
||||||
|
try:
|
||||||
|
del os.environ[key]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
os.environ[key] = value
|
||||||
|
yield (bytes_output, not self.mix_stderr and bytes_error)
|
||||||
|
finally:
|
||||||
|
for key, value in iteritems(old_env):
|
||||||
|
if value is None:
|
||||||
|
try:
|
||||||
|
del os.environ[key]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
os.environ[key] = value
|
||||||
|
sys.stdout = old_stdout
|
||||||
|
sys.stderr = old_stderr
|
||||||
|
sys.stdin = old_stdin
|
||||||
|
termui.visible_prompt_func = old_visible_prompt_func
|
||||||
|
termui.hidden_prompt_func = old_hidden_prompt_func
|
||||||
|
termui._getchar = old__getchar_func
|
||||||
|
utils.should_strip_ansi = old_should_strip_ansi
|
||||||
|
formatting.FORCED_WIDTH = old_forced_width
|
||||||
|
|
||||||
|
def invoke(
|
||||||
|
self,
|
||||||
|
cli,
|
||||||
|
args=None,
|
||||||
|
input=None,
|
||||||
|
env=None,
|
||||||
|
catch_exceptions=True,
|
||||||
|
color=False,
|
||||||
|
**extra
|
||||||
|
):
|
||||||
|
"""Invokes a command in an isolated environment. The arguments are
|
||||||
|
forwarded directly to the command line script, the `extra` keyword
|
||||||
|
arguments are passed to the :meth:`~clickpkg.Command.main` function of
|
||||||
|
the command.
|
||||||
|
|
||||||
|
This returns a :class:`Result` object.
|
||||||
|
|
||||||
|
.. versionadded:: 3.0
|
||||||
|
The ``catch_exceptions`` parameter was added.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.0
|
||||||
|
The result object now has an `exc_info` attribute with the
|
||||||
|
traceback if available.
|
||||||
|
|
||||||
|
.. versionadded:: 4.0
|
||||||
|
The ``color`` parameter was added.
|
||||||
|
|
||||||
|
:param cli: the command to invoke
|
||||||
|
:param args: the arguments to invoke. It may be given as an iterable
|
||||||
|
or a string. When given as string it will be interpreted
|
||||||
|
as a Unix shell command. More details at
|
||||||
|
:func:`shlex.split`.
|
||||||
|
:param input: the input data for `sys.stdin`.
|
||||||
|
:param env: the environment overrides.
|
||||||
|
:param catch_exceptions: Whether to catch any other exceptions than
|
||||||
|
``SystemExit``.
|
||||||
|
:param extra: the keyword arguments to pass to :meth:`main`.
|
||||||
|
:param color: whether the output should contain color codes. The
|
||||||
|
application can still override this explicitly.
|
||||||
|
"""
|
||||||
|
exc_info = None
|
||||||
|
with self.isolation(input=input, env=env, color=color) as outstreams:
|
||||||
|
exception = None
|
||||||
|
exit_code = 0
|
||||||
|
|
||||||
|
if isinstance(args, string_types):
|
||||||
|
args = shlex.split(args)
|
||||||
|
|
||||||
|
try:
|
||||||
|
prog_name = extra.pop("prog_name")
|
||||||
|
except KeyError:
|
||||||
|
prog_name = self.get_default_prog_name(cli)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cli.main(args=args or (), prog_name=prog_name, **extra)
|
||||||
|
except SystemExit as e:
|
||||||
|
exc_info = sys.exc_info()
|
||||||
|
exit_code = e.code
|
||||||
|
if exit_code is None:
|
||||||
|
exit_code = 0
|
||||||
|
|
||||||
|
if exit_code != 0:
|
||||||
|
exception = e
|
||||||
|
|
||||||
|
if not isinstance(exit_code, int):
|
||||||
|
sys.stdout.write(str(exit_code))
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
exit_code = 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if not catch_exceptions:
|
||||||
|
raise
|
||||||
|
exception = e
|
||||||
|
exit_code = 1
|
||||||
|
exc_info = sys.exc_info()
|
||||||
|
finally:
|
||||||
|
sys.stdout.flush()
|
||||||
|
stdout = outstreams[0].getvalue()
|
||||||
|
if self.mix_stderr:
|
||||||
|
stderr = None
|
||||||
|
else:
|
||||||
|
stderr = outstreams[1].getvalue()
|
||||||
|
|
||||||
|
return Result(
|
||||||
|
runner=self,
|
||||||
|
stdout_bytes=stdout,
|
||||||
|
stderr_bytes=stderr,
|
||||||
|
exit_code=exit_code,
|
||||||
|
exception=exception,
|
||||||
|
exc_info=exc_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def isolated_filesystem(self):
|
||||||
|
"""A context manager that creates a temporary folder and changes
|
||||||
|
the current working directory to it for isolated filesystem tests.
|
||||||
|
"""
|
||||||
|
cwd = os.getcwd()
|
||||||
|
t = tempfile.mkdtemp()
|
||||||
|
os.chdir(t)
|
||||||
|
try:
|
||||||
|
yield t
|
||||||
|
finally:
|
||||||
|
os.chdir(cwd)
|
||||||
|
try:
|
||||||
|
shutil.rmtree(t)
|
||||||
|
except (OSError, IOError): # noqa: B014
|
||||||
|
pass
|
||||||
762
mixly/tools/python/click/types.py
Normal file
@@ -0,0 +1,762 @@
|
|||||||
|
import os
|
||||||
|
import stat
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from ._compat import _get_argv_encoding
|
||||||
|
from ._compat import filename_to_ui
|
||||||
|
from ._compat import get_filesystem_encoding
|
||||||
|
from ._compat import get_streerror
|
||||||
|
from ._compat import open_stream
|
||||||
|
from ._compat import PY2
|
||||||
|
from ._compat import text_type
|
||||||
|
from .exceptions import BadParameter
|
||||||
|
from .utils import LazyFile
|
||||||
|
from .utils import safecall
|
||||||
|
|
||||||
|
|
||||||
|
class ParamType(object):
|
||||||
|
"""Helper for converting values through types. The following is
|
||||||
|
necessary for a valid type:
|
||||||
|
|
||||||
|
* it needs a name
|
||||||
|
* it needs to pass through None unchanged
|
||||||
|
* it needs to convert from a string
|
||||||
|
* it needs to convert its result type through unchanged
|
||||||
|
(eg: needs to be idempotent)
|
||||||
|
* it needs to be able to deal with param and context being `None`.
|
||||||
|
This can be the case when the object is used with prompt
|
||||||
|
inputs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
is_composite = False
|
||||||
|
|
||||||
|
#: the descriptive name of this type
|
||||||
|
name = None
|
||||||
|
|
||||||
|
#: if a list of this type is expected and the value is pulled from a
|
||||||
|
#: string environment variable, this is what splits it up. `None`
|
||||||
|
#: means any whitespace. For all parameters the general rule is that
|
||||||
|
#: whitespace splits them up. The exception are paths and files which
|
||||||
|
#: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on
|
||||||
|
#: Windows).
|
||||||
|
envvar_list_splitter = None
|
||||||
|
|
||||||
|
def __call__(self, value, param=None, ctx=None):
|
||||||
|
if value is not None:
|
||||||
|
return self.convert(value, param, ctx)
|
||||||
|
|
||||||
|
def get_metavar(self, param):
|
||||||
|
"""Returns the metavar default for this param if it provides one."""
|
||||||
|
|
||||||
|
def get_missing_message(self, param):
|
||||||
|
"""Optionally might return extra information about a missing
|
||||||
|
parameter.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
"""Converts the value. This is not invoked for values that are
|
||||||
|
`None` (the missing value).
|
||||||
|
"""
|
||||||
|
return value
|
||||||
|
|
||||||
|
def split_envvar_value(self, rv):
|
||||||
|
"""Given a value from an environment variable this splits it up
|
||||||
|
into small chunks depending on the defined envvar list splitter.
|
||||||
|
|
||||||
|
If the splitter is set to `None`, which means that whitespace splits,
|
||||||
|
then leading and trailing whitespace is ignored. Otherwise, leading
|
||||||
|
and trailing splitters usually lead to empty items being included.
|
||||||
|
"""
|
||||||
|
return (rv or "").split(self.envvar_list_splitter)
|
||||||
|
|
||||||
|
def fail(self, message, param=None, ctx=None):
|
||||||
|
"""Helper method to fail with an invalid value message."""
|
||||||
|
raise BadParameter(message, ctx=ctx, param=param)
|
||||||
|
|
||||||
|
|
||||||
|
class CompositeParamType(ParamType):
|
||||||
|
is_composite = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def arity(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class FuncParamType(ParamType):
|
||||||
|
def __init__(self, func):
|
||||||
|
self.name = func.__name__
|
||||||
|
self.func = func
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
try:
|
||||||
|
return self.func(value)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
value = text_type(value)
|
||||||
|
except UnicodeError:
|
||||||
|
value = str(value).decode("utf-8", "replace")
|
||||||
|
self.fail(value, param, ctx)
|
||||||
|
|
||||||
|
|
||||||
|
class UnprocessedParamType(ParamType):
|
||||||
|
name = "text"
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
return value
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "UNPROCESSED"
|
||||||
|
|
||||||
|
|
||||||
|
class StringParamType(ParamType):
|
||||||
|
name = "text"
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
enc = _get_argv_encoding()
|
||||||
|
try:
|
||||||
|
value = value.decode(enc)
|
||||||
|
except UnicodeError:
|
||||||
|
fs_enc = get_filesystem_encoding()
|
||||||
|
if fs_enc != enc:
|
||||||
|
try:
|
||||||
|
value = value.decode(fs_enc)
|
||||||
|
except UnicodeError:
|
||||||
|
value = value.decode("utf-8", "replace")
|
||||||
|
else:
|
||||||
|
value = value.decode("utf-8", "replace")
|
||||||
|
return value
|
||||||
|
return value
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "STRING"
|
||||||
|
|
||||||
|
|
||||||
|
class Choice(ParamType):
|
||||||
|
"""The choice type allows a value to be checked against a fixed set
|
||||||
|
of supported values. All of these values have to be strings.
|
||||||
|
|
||||||
|
You should only pass a list or tuple of choices. Other iterables
|
||||||
|
(like generators) may lead to surprising results.
|
||||||
|
|
||||||
|
The resulting value will always be one of the originally passed choices
|
||||||
|
regardless of ``case_sensitive`` or any ``ctx.token_normalize_func``
|
||||||
|
being specified.
|
||||||
|
|
||||||
|
See :ref:`choice-opts` for an example.
|
||||||
|
|
||||||
|
:param case_sensitive: Set to false to make choices case
|
||||||
|
insensitive. Defaults to true.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "choice"
|
||||||
|
|
||||||
|
def __init__(self, choices, case_sensitive=True):
|
||||||
|
self.choices = choices
|
||||||
|
self.case_sensitive = case_sensitive
|
||||||
|
|
||||||
|
def get_metavar(self, param):
|
||||||
|
return "[{}]".format("|".join(self.choices))
|
||||||
|
|
||||||
|
def get_missing_message(self, param):
|
||||||
|
return "Choose from:\n\t{}.".format(",\n\t".join(self.choices))
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
# Match through normalization and case sensitivity
|
||||||
|
# first do token_normalize_func, then lowercase
|
||||||
|
# preserve original `value` to produce an accurate message in
|
||||||
|
# `self.fail`
|
||||||
|
normed_value = value
|
||||||
|
normed_choices = {choice: choice for choice in self.choices}
|
||||||
|
|
||||||
|
if ctx is not None and ctx.token_normalize_func is not None:
|
||||||
|
normed_value = ctx.token_normalize_func(value)
|
||||||
|
normed_choices = {
|
||||||
|
ctx.token_normalize_func(normed_choice): original
|
||||||
|
for normed_choice, original in normed_choices.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
if not self.case_sensitive:
|
||||||
|
if PY2:
|
||||||
|
lower = str.lower
|
||||||
|
else:
|
||||||
|
lower = str.casefold
|
||||||
|
|
||||||
|
normed_value = lower(normed_value)
|
||||||
|
normed_choices = {
|
||||||
|
lower(normed_choice): original
|
||||||
|
for normed_choice, original in normed_choices.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
if normed_value in normed_choices:
|
||||||
|
return normed_choices[normed_value]
|
||||||
|
|
||||||
|
self.fail(
|
||||||
|
"invalid choice: {}. (choose from {})".format(
|
||||||
|
value, ", ".join(self.choices)
|
||||||
|
),
|
||||||
|
param,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "Choice('{}')".format(list(self.choices))
|
||||||
|
|
||||||
|
|
||||||
|
class DateTime(ParamType):
|
||||||
|
"""The DateTime type converts date strings into `datetime` objects.
|
||||||
|
|
||||||
|
The format strings which are checked are configurable, but default to some
|
||||||
|
common (non-timezone aware) ISO 8601 formats.
|
||||||
|
|
||||||
|
When specifying *DateTime* formats, you should only pass a list or a tuple.
|
||||||
|
Other iterables, like generators, may lead to surprising results.
|
||||||
|
|
||||||
|
The format strings are processed using ``datetime.strptime``, and this
|
||||||
|
consequently defines the format strings which are allowed.
|
||||||
|
|
||||||
|
Parsing is tried using each format, in order, and the first format which
|
||||||
|
parses successfully is used.
|
||||||
|
|
||||||
|
:param formats: A list or tuple of date format strings, in the order in
|
||||||
|
which they should be tried. Defaults to
|
||||||
|
``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``,
|
||||||
|
``'%Y-%m-%d %H:%M:%S'``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "datetime"
|
||||||
|
|
||||||
|
def __init__(self, formats=None):
|
||||||
|
self.formats = formats or ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"]
|
||||||
|
|
||||||
|
def get_metavar(self, param):
|
||||||
|
return "[{}]".format("|".join(self.formats))
|
||||||
|
|
||||||
|
def _try_to_convert_date(self, value, format):
|
||||||
|
try:
|
||||||
|
return datetime.strptime(value, format)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
# Exact match
|
||||||
|
for format in self.formats:
|
||||||
|
dtime = self._try_to_convert_date(value, format)
|
||||||
|
if dtime:
|
||||||
|
return dtime
|
||||||
|
|
||||||
|
self.fail(
|
||||||
|
"invalid datetime format: {}. (choose from {})".format(
|
||||||
|
value, ", ".join(self.formats)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "DateTime"
|
||||||
|
|
||||||
|
|
||||||
|
class IntParamType(ParamType):
|
||||||
|
name = "integer"
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except ValueError:
|
||||||
|
self.fail("{} is not a valid integer".format(value), param, ctx)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "INT"
|
||||||
|
|
||||||
|
|
||||||
|
class IntRange(IntParamType):
|
||||||
|
"""A parameter that works similar to :data:`click.INT` but restricts
|
||||||
|
the value to fit into a range. The default behavior is to fail if the
|
||||||
|
value falls outside the range, but it can also be silently clamped
|
||||||
|
between the two edges.
|
||||||
|
|
||||||
|
See :ref:`ranges` for an example.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "integer range"
|
||||||
|
|
||||||
|
def __init__(self, min=None, max=None, clamp=False):
|
||||||
|
self.min = min
|
||||||
|
self.max = max
|
||||||
|
self.clamp = clamp
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
rv = IntParamType.convert(self, value, param, ctx)
|
||||||
|
if self.clamp:
|
||||||
|
if self.min is not None and rv < self.min:
|
||||||
|
return self.min
|
||||||
|
if self.max is not None and rv > self.max:
|
||||||
|
return self.max
|
||||||
|
if (
|
||||||
|
self.min is not None
|
||||||
|
and rv < self.min
|
||||||
|
or self.max is not None
|
||||||
|
and rv > self.max
|
||||||
|
):
|
||||||
|
if self.min is None:
|
||||||
|
self.fail(
|
||||||
|
"{} is bigger than the maximum valid value {}.".format(
|
||||||
|
rv, self.max
|
||||||
|
),
|
||||||
|
param,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
elif self.max is None:
|
||||||
|
self.fail(
|
||||||
|
"{} is smaller than the minimum valid value {}.".format(
|
||||||
|
rv, self.min
|
||||||
|
),
|
||||||
|
param,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.fail(
|
||||||
|
"{} is not in the valid range of {} to {}.".format(
|
||||||
|
rv, self.min, self.max
|
||||||
|
),
|
||||||
|
param,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "IntRange({}, {})".format(self.min, self.max)
|
||||||
|
|
||||||
|
|
||||||
|
class FloatParamType(ParamType):
|
||||||
|
name = "float"
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except ValueError:
|
||||||
|
self.fail(
|
||||||
|
"{} is not a valid floating point value".format(value), param, ctx
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "FLOAT"
|
||||||
|
|
||||||
|
|
||||||
|
class FloatRange(FloatParamType):
|
||||||
|
"""A parameter that works similar to :data:`click.FLOAT` but restricts
|
||||||
|
the value to fit into a range. The default behavior is to fail if the
|
||||||
|
value falls outside the range, but it can also be silently clamped
|
||||||
|
between the two edges.
|
||||||
|
|
||||||
|
See :ref:`ranges` for an example.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "float range"
|
||||||
|
|
||||||
|
def __init__(self, min=None, max=None, clamp=False):
|
||||||
|
self.min = min
|
||||||
|
self.max = max
|
||||||
|
self.clamp = clamp
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
rv = FloatParamType.convert(self, value, param, ctx)
|
||||||
|
if self.clamp:
|
||||||
|
if self.min is not None and rv < self.min:
|
||||||
|
return self.min
|
||||||
|
if self.max is not None and rv > self.max:
|
||||||
|
return self.max
|
||||||
|
if (
|
||||||
|
self.min is not None
|
||||||
|
and rv < self.min
|
||||||
|
or self.max is not None
|
||||||
|
and rv > self.max
|
||||||
|
):
|
||||||
|
if self.min is None:
|
||||||
|
self.fail(
|
||||||
|
"{} is bigger than the maximum valid value {}.".format(
|
||||||
|
rv, self.max
|
||||||
|
),
|
||||||
|
param,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
elif self.max is None:
|
||||||
|
self.fail(
|
||||||
|
"{} is smaller than the minimum valid value {}.".format(
|
||||||
|
rv, self.min
|
||||||
|
),
|
||||||
|
param,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.fail(
|
||||||
|
"{} is not in the valid range of {} to {}.".format(
|
||||||
|
rv, self.min, self.max
|
||||||
|
),
|
||||||
|
param,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "FloatRange({}, {})".format(self.min, self.max)
|
||||||
|
|
||||||
|
|
||||||
|
class BoolParamType(ParamType):
|
||||||
|
name = "boolean"
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return bool(value)
|
||||||
|
value = value.lower()
|
||||||
|
if value in ("true", "t", "1", "yes", "y"):
|
||||||
|
return True
|
||||||
|
elif value in ("false", "f", "0", "no", "n"):
|
||||||
|
return False
|
||||||
|
self.fail("{} is not a valid boolean".format(value), param, ctx)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "BOOL"
|
||||||
|
|
||||||
|
|
||||||
|
class UUIDParameterType(ParamType):
|
||||||
|
name = "uuid"
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
try:
|
||||||
|
if PY2 and isinstance(value, text_type):
|
||||||
|
value = value.encode("ascii")
|
||||||
|
return uuid.UUID(value)
|
||||||
|
except ValueError:
|
||||||
|
self.fail("{} is not a valid UUID value".format(value), param, ctx)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "UUID"
|
||||||
|
|
||||||
|
|
||||||
|
class File(ParamType):
|
||||||
|
"""Declares a parameter to be a file for reading or writing. The file
|
||||||
|
is automatically closed once the context tears down (after the command
|
||||||
|
finished working).
|
||||||
|
|
||||||
|
Files can be opened for reading or writing. The special value ``-``
|
||||||
|
indicates stdin or stdout depending on the mode.
|
||||||
|
|
||||||
|
By default, the file is opened for reading text data, but it can also be
|
||||||
|
opened in binary mode or for writing. The encoding parameter can be used
|
||||||
|
to force a specific encoding.
|
||||||
|
|
||||||
|
The `lazy` flag controls if the file should be opened immediately or upon
|
||||||
|
first IO. The default is to be non-lazy for standard input and output
|
||||||
|
streams as well as files opened for reading, `lazy` otherwise. When opening a
|
||||||
|
file lazily for reading, it is still opened temporarily for validation, but
|
||||||
|
will not be held open until first IO. lazy is mainly useful when opening
|
||||||
|
for writing to avoid creating the file until it is needed.
|
||||||
|
|
||||||
|
Starting with Click 2.0, files can also be opened atomically in which
|
||||||
|
case all writes go into a separate file in the same folder and upon
|
||||||
|
completion the file will be moved over to the original location. This
|
||||||
|
is useful if a file regularly read by other users is modified.
|
||||||
|
|
||||||
|
See :ref:`file-args` for more information.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "filename"
|
||||||
|
envvar_list_splitter = os.path.pathsep
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, mode="r", encoding=None, errors="strict", lazy=None, atomic=False
|
||||||
|
):
|
||||||
|
self.mode = mode
|
||||||
|
self.encoding = encoding
|
||||||
|
self.errors = errors
|
||||||
|
self.lazy = lazy
|
||||||
|
self.atomic = atomic
|
||||||
|
|
||||||
|
def resolve_lazy_flag(self, value):
|
||||||
|
if self.lazy is not None:
|
||||||
|
return self.lazy
|
||||||
|
if value == "-":
|
||||||
|
return False
|
||||||
|
elif "w" in self.mode:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
try:
|
||||||
|
if hasattr(value, "read") or hasattr(value, "write"):
|
||||||
|
return value
|
||||||
|
|
||||||
|
lazy = self.resolve_lazy_flag(value)
|
||||||
|
|
||||||
|
if lazy:
|
||||||
|
f = LazyFile(
|
||||||
|
value, self.mode, self.encoding, self.errors, atomic=self.atomic
|
||||||
|
)
|
||||||
|
if ctx is not None:
|
||||||
|
ctx.call_on_close(f.close_intelligently)
|
||||||
|
return f
|
||||||
|
|
||||||
|
f, should_close = open_stream(
|
||||||
|
value, self.mode, self.encoding, self.errors, atomic=self.atomic
|
||||||
|
)
|
||||||
|
# If a context is provided, we automatically close the file
|
||||||
|
# at the end of the context execution (or flush out). If a
|
||||||
|
# context does not exist, it's the caller's responsibility to
|
||||||
|
# properly close the file. This for instance happens when the
|
||||||
|
# type is used with prompts.
|
||||||
|
if ctx is not None:
|
||||||
|
if should_close:
|
||||||
|
ctx.call_on_close(safecall(f.close))
|
||||||
|
else:
|
||||||
|
ctx.call_on_close(safecall(f.flush))
|
||||||
|
return f
|
||||||
|
except (IOError, OSError) as e: # noqa: B014
|
||||||
|
self.fail(
|
||||||
|
"Could not open file: {}: {}".format(
|
||||||
|
filename_to_ui(value), get_streerror(e)
|
||||||
|
),
|
||||||
|
param,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Path(ParamType):
|
||||||
|
"""The path type is similar to the :class:`File` type but it performs
|
||||||
|
different checks. First of all, instead of returning an open file
|
||||||
|
handle it returns just the filename. Secondly, it can perform various
|
||||||
|
basic checks about what the file or directory should be.
|
||||||
|
|
||||||
|
.. versionchanged:: 6.0
|
||||||
|
`allow_dash` was added.
|
||||||
|
|
||||||
|
:param exists: if set to true, the file or directory needs to exist for
|
||||||
|
this value to be valid. If this is not required and a
|
||||||
|
file does indeed not exist, then all further checks are
|
||||||
|
silently skipped.
|
||||||
|
:param file_okay: controls if a file is a possible value.
|
||||||
|
:param dir_okay: controls if a directory is a possible value.
|
||||||
|
:param writable: if true, a writable check is performed.
|
||||||
|
:param readable: if true, a readable check is performed.
|
||||||
|
:param resolve_path: if this is true, then the path is fully resolved
|
||||||
|
before the value is passed onwards. This means
|
||||||
|
that it's absolute and symlinks are resolved. It
|
||||||
|
will not expand a tilde-prefix, as this is
|
||||||
|
supposed to be done by the shell only.
|
||||||
|
:param allow_dash: If this is set to `True`, a single dash to indicate
|
||||||
|
standard streams is permitted.
|
||||||
|
:param path_type: optionally a string type that should be used to
|
||||||
|
represent the path. The default is `None` which
|
||||||
|
means the return value will be either bytes or
|
||||||
|
unicode depending on what makes most sense given the
|
||||||
|
input data Click deals with.
|
||||||
|
"""
|
||||||
|
|
||||||
|
envvar_list_splitter = os.path.pathsep
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
exists=False,
|
||||||
|
file_okay=True,
|
||||||
|
dir_okay=True,
|
||||||
|
writable=False,
|
||||||
|
readable=True,
|
||||||
|
resolve_path=False,
|
||||||
|
allow_dash=False,
|
||||||
|
path_type=None,
|
||||||
|
):
|
||||||
|
self.exists = exists
|
||||||
|
self.file_okay = file_okay
|
||||||
|
self.dir_okay = dir_okay
|
||||||
|
self.writable = writable
|
||||||
|
self.readable = readable
|
||||||
|
self.resolve_path = resolve_path
|
||||||
|
self.allow_dash = allow_dash
|
||||||
|
self.type = path_type
|
||||||
|
|
||||||
|
if self.file_okay and not self.dir_okay:
|
||||||
|
self.name = "file"
|
||||||
|
self.path_type = "File"
|
||||||
|
elif self.dir_okay and not self.file_okay:
|
||||||
|
self.name = "directory"
|
||||||
|
self.path_type = "Directory"
|
||||||
|
else:
|
||||||
|
self.name = "path"
|
||||||
|
self.path_type = "Path"
|
||||||
|
|
||||||
|
def coerce_path_result(self, rv):
|
||||||
|
if self.type is not None and not isinstance(rv, self.type):
|
||||||
|
if self.type is text_type:
|
||||||
|
rv = rv.decode(get_filesystem_encoding())
|
||||||
|
else:
|
||||||
|
rv = rv.encode(get_filesystem_encoding())
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
rv = value
|
||||||
|
|
||||||
|
is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-")
|
||||||
|
|
||||||
|
if not is_dash:
|
||||||
|
if self.resolve_path:
|
||||||
|
rv = os.path.realpath(rv)
|
||||||
|
|
||||||
|
try:
|
||||||
|
st = os.stat(rv)
|
||||||
|
except OSError:
|
||||||
|
if not self.exists:
|
||||||
|
return self.coerce_path_result(rv)
|
||||||
|
self.fail(
|
||||||
|
"{} '{}' does not exist.".format(
|
||||||
|
self.path_type, filename_to_ui(value)
|
||||||
|
),
|
||||||
|
param,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.file_okay and stat.S_ISREG(st.st_mode):
|
||||||
|
self.fail(
|
||||||
|
"{} '{}' is a file.".format(self.path_type, filename_to_ui(value)),
|
||||||
|
param,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
if not self.dir_okay and stat.S_ISDIR(st.st_mode):
|
||||||
|
self.fail(
|
||||||
|
"{} '{}' is a directory.".format(
|
||||||
|
self.path_type, filename_to_ui(value)
|
||||||
|
),
|
||||||
|
param,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
if self.writable and not os.access(value, os.W_OK):
|
||||||
|
self.fail(
|
||||||
|
"{} '{}' is not writable.".format(
|
||||||
|
self.path_type, filename_to_ui(value)
|
||||||
|
),
|
||||||
|
param,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
if self.readable and not os.access(value, os.R_OK):
|
||||||
|
self.fail(
|
||||||
|
"{} '{}' is not readable.".format(
|
||||||
|
self.path_type, filename_to_ui(value)
|
||||||
|
),
|
||||||
|
param,
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.coerce_path_result(rv)
|
||||||
|
|
||||||
|
|
||||||
|
class Tuple(CompositeParamType):
|
||||||
|
"""The default behavior of Click is to apply a type on a value directly.
|
||||||
|
This works well in most cases, except for when `nargs` is set to a fixed
|
||||||
|
count and different types should be used for different items. In this
|
||||||
|
case the :class:`Tuple` type can be used. This type can only be used
|
||||||
|
if `nargs` is set to a fixed number.
|
||||||
|
|
||||||
|
For more information see :ref:`tuple-type`.
|
||||||
|
|
||||||
|
This can be selected by using a Python tuple literal as a type.
|
||||||
|
|
||||||
|
:param types: a list of types that should be used for the tuple items.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, types):
|
||||||
|
self.types = [convert_type(ty) for ty in types]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return "<{}>".format(" ".join(ty.name for ty in self.types))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def arity(self):
|
||||||
|
return len(self.types)
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
if len(value) != len(self.types):
|
||||||
|
raise TypeError(
|
||||||
|
"It would appear that nargs is set to conflict with the"
|
||||||
|
" composite type arity."
|
||||||
|
)
|
||||||
|
return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value))
|
||||||
|
|
||||||
|
|
||||||
|
def convert_type(ty, default=None):
|
||||||
|
"""Converts a callable or python type into the most appropriate
|
||||||
|
param type.
|
||||||
|
"""
|
||||||
|
guessed_type = False
|
||||||
|
if ty is None and default is not None:
|
||||||
|
if isinstance(default, tuple):
|
||||||
|
ty = tuple(map(type, default))
|
||||||
|
else:
|
||||||
|
ty = type(default)
|
||||||
|
guessed_type = True
|
||||||
|
|
||||||
|
if isinstance(ty, tuple):
|
||||||
|
return Tuple(ty)
|
||||||
|
if isinstance(ty, ParamType):
|
||||||
|
return ty
|
||||||
|
if ty is text_type or ty is str or ty is None:
|
||||||
|
return STRING
|
||||||
|
if ty is int:
|
||||||
|
return INT
|
||||||
|
# Booleans are only okay if not guessed. This is done because for
|
||||||
|
# flags the default value is actually a bit of a lie in that it
|
||||||
|
# indicates which of the flags is the one we want. See get_default()
|
||||||
|
# for more information.
|
||||||
|
if ty is bool and not guessed_type:
|
||||||
|
return BOOL
|
||||||
|
if ty is float:
|
||||||
|
return FLOAT
|
||||||
|
if guessed_type:
|
||||||
|
return STRING
|
||||||
|
|
||||||
|
# Catch a common mistake
|
||||||
|
if __debug__:
|
||||||
|
try:
|
||||||
|
if issubclass(ty, ParamType):
|
||||||
|
raise AssertionError(
|
||||||
|
"Attempted to use an uninstantiated parameter type ({}).".format(ty)
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
return FuncParamType(ty)
|
||||||
|
|
||||||
|
|
||||||
|
#: A dummy parameter type that just does nothing. From a user's
|
||||||
|
#: perspective this appears to just be the same as `STRING` but internally
|
||||||
|
#: no string conversion takes place. This is necessary to achieve the
|
||||||
|
#: same bytes/unicode behavior on Python 2/3 in situations where you want
|
||||||
|
#: to not convert argument types. This is usually useful when working
|
||||||
|
#: with file paths as they can appear in bytes and unicode.
|
||||||
|
#:
|
||||||
|
#: For path related uses the :class:`Path` type is a better choice but
|
||||||
|
#: there are situations where an unprocessed type is useful which is why
|
||||||
|
#: it is is provided.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 4.0
|
||||||
|
UNPROCESSED = UnprocessedParamType()
|
||||||
|
|
||||||
|
#: A unicode string parameter type which is the implicit default. This
|
||||||
|
#: can also be selected by using ``str`` as type.
|
||||||
|
STRING = StringParamType()
|
||||||
|
|
||||||
|
#: An integer parameter. This can also be selected by using ``int`` as
|
||||||
|
#: type.
|
||||||
|
INT = IntParamType()
|
||||||
|
|
||||||
|
#: A floating point value parameter. This can also be selected by using
|
||||||
|
#: ``float`` as type.
|
||||||
|
FLOAT = FloatParamType()
|
||||||
|
|
||||||
|
#: A boolean parameter. This is the default for boolean flags. This can
|
||||||
|
#: also be selected by using ``bool`` as a type.
|
||||||
|
BOOL = BoolParamType()
|
||||||
|
|
||||||
|
#: A UUID parameter.
|
||||||
|
UUID = UUIDParameterType()
|
||||||
455
mixly/tools/python/click/utils.py
Normal file
@@ -0,0 +1,455 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ._compat import _default_text_stderr
|
||||||
|
from ._compat import _default_text_stdout
|
||||||
|
from ._compat import auto_wrap_for_ansi
|
||||||
|
from ._compat import binary_streams
|
||||||
|
from ._compat import filename_to_ui
|
||||||
|
from ._compat import get_filesystem_encoding
|
||||||
|
from ._compat import get_streerror
|
||||||
|
from ._compat import is_bytes
|
||||||
|
from ._compat import open_stream
|
||||||
|
from ._compat import PY2
|
||||||
|
from ._compat import should_strip_ansi
|
||||||
|
from ._compat import string_types
|
||||||
|
from ._compat import strip_ansi
|
||||||
|
from ._compat import text_streams
|
||||||
|
from ._compat import text_type
|
||||||
|
from ._compat import WIN
|
||||||
|
from .globals import resolve_color_default
|
||||||
|
|
||||||
|
if not PY2:
|
||||||
|
from ._compat import _find_binary_writer
|
||||||
|
elif WIN:
|
||||||
|
from ._winconsole import _get_windows_argv
|
||||||
|
from ._winconsole import _hash_py_argv
|
||||||
|
from ._winconsole import _initial_argv_hash
|
||||||
|
|
||||||
|
echo_native_types = string_types + (bytes, bytearray)
|
||||||
|
|
||||||
|
|
||||||
|
def _posixify(name):
|
||||||
|
return "-".join(name.split()).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def safecall(func):
|
||||||
|
"""Wraps a function so that it swallows exceptions."""
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def make_str(value):
|
||||||
|
"""Converts a value into a valid string."""
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
try:
|
||||||
|
return value.decode(get_filesystem_encoding())
|
||||||
|
except UnicodeError:
|
||||||
|
return value.decode("utf-8", "replace")
|
||||||
|
return text_type(value)
|
||||||
|
|
||||||
|
|
||||||
|
def make_default_short_help(help, max_length=45):
|
||||||
|
"""Return a condensed version of help string."""
|
||||||
|
words = help.split()
|
||||||
|
total_length = 0
|
||||||
|
result = []
|
||||||
|
done = False
|
||||||
|
|
||||||
|
for word in words:
|
||||||
|
if word[-1:] == ".":
|
||||||
|
done = True
|
||||||
|
new_length = 1 + len(word) if result else len(word)
|
||||||
|
if total_length + new_length > max_length:
|
||||||
|
result.append("...")
|
||||||
|
done = True
|
||||||
|
else:
|
||||||
|
if result:
|
||||||
|
result.append(" ")
|
||||||
|
result.append(word)
|
||||||
|
if done:
|
||||||
|
break
|
||||||
|
total_length += new_length
|
||||||
|
|
||||||
|
return "".join(result)
|
||||||
|
|
||||||
|
|
||||||
|
class LazyFile(object):
|
||||||
|
"""A lazy file works like a regular file but it does not fully open
|
||||||
|
the file but it does perform some basic checks early to see if the
|
||||||
|
filename parameter does make sense. This is useful for safely opening
|
||||||
|
files for writing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, filename, mode="r", encoding=None, errors="strict", atomic=False
|
||||||
|
):
|
||||||
|
self.name = filename
|
||||||
|
self.mode = mode
|
||||||
|
self.encoding = encoding
|
||||||
|
self.errors = errors
|
||||||
|
self.atomic = atomic
|
||||||
|
|
||||||
|
if filename == "-":
|
||||||
|
self._f, self.should_close = open_stream(filename, mode, encoding, errors)
|
||||||
|
else:
|
||||||
|
if "r" in mode:
|
||||||
|
# Open and close the file in case we're opening it for
|
||||||
|
# reading so that we can catch at least some errors in
|
||||||
|
# some cases early.
|
||||||
|
open(filename, mode).close()
|
||||||
|
self._f = None
|
||||||
|
self.should_close = True
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self.open(), name)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self._f is not None:
|
||||||
|
return repr(self._f)
|
||||||
|
return "<unopened file '{}' {}>".format(self.name, self.mode)
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
"""Opens the file if it's not yet open. This call might fail with
|
||||||
|
a :exc:`FileError`. Not handling this error will produce an error
|
||||||
|
that Click shows.
|
||||||
|
"""
|
||||||
|
if self._f is not None:
|
||||||
|
return self._f
|
||||||
|
try:
|
||||||
|
rv, self.should_close = open_stream(
|
||||||
|
self.name, self.mode, self.encoding, self.errors, atomic=self.atomic
|
||||||
|
)
|
||||||
|
except (IOError, OSError) as e: # noqa: E402
|
||||||
|
from .exceptions import FileError
|
||||||
|
|
||||||
|
raise FileError(self.name, hint=get_streerror(e))
|
||||||
|
self._f = rv
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Closes the underlying file, no matter what."""
|
||||||
|
if self._f is not None:
|
||||||
|
self._f.close()
|
||||||
|
|
||||||
|
def close_intelligently(self):
|
||||||
|
"""This function only closes the file if it was opened by the lazy
|
||||||
|
file wrapper. For instance this will never close stdin.
|
||||||
|
"""
|
||||||
|
if self.should_close:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, tb):
|
||||||
|
self.close_intelligently()
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
self.open()
|
||||||
|
return iter(self._f)
|
||||||
|
|
||||||
|
|
||||||
|
class KeepOpenFile(object):
|
||||||
|
def __init__(self, file):
|
||||||
|
self._file = file
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self._file, name)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, tb):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return repr(self._file)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._file)
|
||||||
|
|
||||||
|
|
||||||
|
def echo(message=None, file=None, nl=True, err=False, color=None):
|
||||||
|
"""Prints a message plus a newline to the given file or stdout. On
|
||||||
|
first sight, this looks like the print function, but it has improved
|
||||||
|
support for handling Unicode and binary data that does not fail no
|
||||||
|
matter how badly configured the system is.
|
||||||
|
|
||||||
|
Primarily it means that you can print binary data as well as Unicode
|
||||||
|
data on both 2.x and 3.x to the given file in the most appropriate way
|
||||||
|
possible. This is a very carefree function in that it will try its
|
||||||
|
best to not fail. As of Click 6.0 this includes support for unicode
|
||||||
|
output on the Windows console.
|
||||||
|
|
||||||
|
In addition to that, if `colorama`_ is installed, the echo function will
|
||||||
|
also support clever handling of ANSI codes. Essentially it will then
|
||||||
|
do the following:
|
||||||
|
|
||||||
|
- add transparent handling of ANSI color codes on Windows.
|
||||||
|
- hide ANSI codes automatically if the destination file is not a
|
||||||
|
terminal.
|
||||||
|
|
||||||
|
.. _colorama: https://pypi.org/project/colorama/
|
||||||
|
|
||||||
|
.. versionchanged:: 6.0
|
||||||
|
As of Click 6.0 the echo function will properly support unicode
|
||||||
|
output on the windows console. Not that click does not modify
|
||||||
|
the interpreter in any way which means that `sys.stdout` or the
|
||||||
|
print statement or function will still not provide unicode support.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
Starting with version 2.0 of Click, the echo function will work
|
||||||
|
with colorama if it's installed.
|
||||||
|
|
||||||
|
.. versionadded:: 3.0
|
||||||
|
The `err` parameter was added.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.0
|
||||||
|
Added the `color` flag.
|
||||||
|
|
||||||
|
:param message: the message to print
|
||||||
|
:param file: the file to write to (defaults to ``stdout``)
|
||||||
|
:param err: if set to true the file defaults to ``stderr`` instead of
|
||||||
|
``stdout``. This is faster and easier than calling
|
||||||
|
:func:`get_text_stderr` yourself.
|
||||||
|
:param nl: if set to `True` (the default) a newline is printed afterwards.
|
||||||
|
:param color: controls if the terminal supports ANSI colors or not. The
|
||||||
|
default is autodetection.
|
||||||
|
"""
|
||||||
|
if file is None:
|
||||||
|
if err:
|
||||||
|
file = _default_text_stderr()
|
||||||
|
else:
|
||||||
|
file = _default_text_stdout()
|
||||||
|
|
||||||
|
# Convert non bytes/text into the native string type.
|
||||||
|
if message is not None and not isinstance(message, echo_native_types):
|
||||||
|
message = text_type(message)
|
||||||
|
|
||||||
|
if nl:
|
||||||
|
message = message or u""
|
||||||
|
if isinstance(message, text_type):
|
||||||
|
message += u"\n"
|
||||||
|
else:
|
||||||
|
message += b"\n"
|
||||||
|
|
||||||
|
# If there is a message, and we're in Python 3, and the value looks
|
||||||
|
# like bytes, we manually need to find the binary stream and write the
|
||||||
|
# message in there. This is done separately so that most stream
|
||||||
|
# types will work as you would expect. Eg: you can write to StringIO
|
||||||
|
# for other cases.
|
||||||
|
if message and not PY2 and is_bytes(message):
|
||||||
|
binary_file = _find_binary_writer(file)
|
||||||
|
if binary_file is not None:
|
||||||
|
file.flush()
|
||||||
|
binary_file.write(message)
|
||||||
|
binary_file.flush()
|
||||||
|
return
|
||||||
|
|
||||||
|
# ANSI-style support. If there is no message or we are dealing with
|
||||||
|
# bytes nothing is happening. If we are connected to a file we want
|
||||||
|
# to strip colors. If we are on windows we either wrap the stream
|
||||||
|
# to strip the color or we use the colorama support to translate the
|
||||||
|
# ansi codes to API calls.
|
||||||
|
if message and not is_bytes(message):
|
||||||
|
color = resolve_color_default(color)
|
||||||
|
if should_strip_ansi(file, color):
|
||||||
|
message = strip_ansi(message)
|
||||||
|
elif WIN:
|
||||||
|
if auto_wrap_for_ansi is not None:
|
||||||
|
file = auto_wrap_for_ansi(file)
|
||||||
|
elif not color:
|
||||||
|
message = strip_ansi(message)
|
||||||
|
|
||||||
|
if message:
|
||||||
|
file.write(message)
|
||||||
|
file.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def get_binary_stream(name):
|
||||||
|
"""Returns a system stream for byte processing. This essentially
|
||||||
|
returns the stream from the sys module with the given name but it
|
||||||
|
solves some compatibility issues between different Python versions.
|
||||||
|
Primarily this function is necessary for getting binary streams on
|
||||||
|
Python 3.
|
||||||
|
|
||||||
|
:param name: the name of the stream to open. Valid names are ``'stdin'``,
|
||||||
|
``'stdout'`` and ``'stderr'``
|
||||||
|
"""
|
||||||
|
opener = binary_streams.get(name)
|
||||||
|
if opener is None:
|
||||||
|
raise TypeError("Unknown standard stream '{}'".format(name))
|
||||||
|
return opener()
|
||||||
|
|
||||||
|
|
||||||
|
def get_text_stream(name, encoding=None, errors="strict"):
|
||||||
|
"""Returns a system stream for text processing. This usually returns
|
||||||
|
a wrapped stream around a binary stream returned from
|
||||||
|
:func:`get_binary_stream` but it also can take shortcuts on Python 3
|
||||||
|
for already correctly configured streams.
|
||||||
|
|
||||||
|
:param name: the name of the stream to open. Valid names are ``'stdin'``,
|
||||||
|
``'stdout'`` and ``'stderr'``
|
||||||
|
:param encoding: overrides the detected default encoding.
|
||||||
|
:param errors: overrides the default error mode.
|
||||||
|
"""
|
||||||
|
opener = text_streams.get(name)
|
||||||
|
if opener is None:
|
||||||
|
raise TypeError("Unknown standard stream '{}'".format(name))
|
||||||
|
return opener(encoding, errors)
|
||||||
|
|
||||||
|
|
||||||
|
def open_file(
|
||||||
|
filename, mode="r", encoding=None, errors="strict", lazy=False, atomic=False
|
||||||
|
):
|
||||||
|
"""This is similar to how the :class:`File` works but for manual
|
||||||
|
usage. Files are opened non lazy by default. This can open regular
|
||||||
|
files as well as stdin/stdout if ``'-'`` is passed.
|
||||||
|
|
||||||
|
If stdin/stdout is returned the stream is wrapped so that the context
|
||||||
|
manager will not close the stream accidentally. This makes it possible
|
||||||
|
to always use the function like this without having to worry to
|
||||||
|
accidentally close a standard stream::
|
||||||
|
|
||||||
|
with open_file(filename) as f:
|
||||||
|
...
|
||||||
|
|
||||||
|
.. versionadded:: 3.0
|
||||||
|
|
||||||
|
:param filename: the name of the file to open (or ``'-'`` for stdin/stdout).
|
||||||
|
:param mode: the mode in which to open the file.
|
||||||
|
:param encoding: the encoding to use.
|
||||||
|
:param errors: the error handling for this file.
|
||||||
|
:param lazy: can be flipped to true to open the file lazily.
|
||||||
|
:param atomic: in atomic mode writes go into a temporary file and it's
|
||||||
|
moved on close.
|
||||||
|
"""
|
||||||
|
if lazy:
|
||||||
|
return LazyFile(filename, mode, encoding, errors, atomic=atomic)
|
||||||
|
f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic)
|
||||||
|
if not should_close:
|
||||||
|
f = KeepOpenFile(f)
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def get_os_args():
|
||||||
|
"""This returns the argument part of sys.argv in the most appropriate
|
||||||
|
form for processing. What this means is that this return value is in
|
||||||
|
a format that works for Click to process but does not necessarily
|
||||||
|
correspond well to what's actually standard for the interpreter.
|
||||||
|
|
||||||
|
On most environments the return value is ``sys.argv[:1]`` unchanged.
|
||||||
|
However if you are on Windows and running Python 2 the return value
|
||||||
|
will actually be a list of unicode strings instead because the
|
||||||
|
default behavior on that platform otherwise will not be able to
|
||||||
|
carry all possible values that sys.argv can have.
|
||||||
|
|
||||||
|
.. versionadded:: 6.0
|
||||||
|
"""
|
||||||
|
# We can only extract the unicode argv if sys.argv has not been
|
||||||
|
# changed since the startup of the application.
|
||||||
|
if PY2 and WIN and _initial_argv_hash == _hash_py_argv():
|
||||||
|
return _get_windows_argv()
|
||||||
|
return sys.argv[1:]
|
||||||
|
|
||||||
|
|
||||||
|
def format_filename(filename, shorten=False):
|
||||||
|
"""Formats a filename for user display. The main purpose of this
|
||||||
|
function is to ensure that the filename can be displayed at all. This
|
||||||
|
will decode the filename to unicode if necessary in a way that it will
|
||||||
|
not fail. Optionally, it can shorten the filename to not include the
|
||||||
|
full path to the filename.
|
||||||
|
|
||||||
|
:param filename: formats a filename for UI display. This will also convert
|
||||||
|
the filename into unicode without failing.
|
||||||
|
:param shorten: this optionally shortens the filename to strip of the
|
||||||
|
path that leads up to it.
|
||||||
|
"""
|
||||||
|
if shorten:
|
||||||
|
filename = os.path.basename(filename)
|
||||||
|
return filename_to_ui(filename)
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_dir(app_name, roaming=True, force_posix=False):
|
||||||
|
r"""Returns the config folder for the application. The default behavior
|
||||||
|
is to return whatever is most appropriate for the operating system.
|
||||||
|
|
||||||
|
To give you an idea, for an app called ``"Foo Bar"``, something like
|
||||||
|
the following folders could be returned:
|
||||||
|
|
||||||
|
Mac OS X:
|
||||||
|
``~/Library/Application Support/Foo Bar``
|
||||||
|
Mac OS X (POSIX):
|
||||||
|
``~/.foo-bar``
|
||||||
|
Unix:
|
||||||
|
``~/.config/foo-bar``
|
||||||
|
Unix (POSIX):
|
||||||
|
``~/.foo-bar``
|
||||||
|
Win XP (roaming):
|
||||||
|
``C:\Documents and Settings\<user>\Local Settings\Application Data\Foo Bar``
|
||||||
|
Win XP (not roaming):
|
||||||
|
``C:\Documents and Settings\<user>\Application Data\Foo Bar``
|
||||||
|
Win 7 (roaming):
|
||||||
|
``C:\Users\<user>\AppData\Roaming\Foo Bar``
|
||||||
|
Win 7 (not roaming):
|
||||||
|
``C:\Users\<user>\AppData\Local\Foo Bar``
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
:param app_name: the application name. This should be properly capitalized
|
||||||
|
and can contain whitespace.
|
||||||
|
:param roaming: controls if the folder should be roaming or not on Windows.
|
||||||
|
Has no affect otherwise.
|
||||||
|
:param force_posix: if this is set to `True` then on any POSIX system the
|
||||||
|
folder will be stored in the home folder with a leading
|
||||||
|
dot instead of the XDG config home or darwin's
|
||||||
|
application support folder.
|
||||||
|
"""
|
||||||
|
if WIN:
|
||||||
|
key = "APPDATA" if roaming else "LOCALAPPDATA"
|
||||||
|
folder = os.environ.get(key)
|
||||||
|
if folder is None:
|
||||||
|
folder = os.path.expanduser("~")
|
||||||
|
return os.path.join(folder, app_name)
|
||||||
|
if force_posix:
|
||||||
|
return os.path.join(os.path.expanduser("~/.{}".format(_posixify(app_name))))
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
return os.path.join(
|
||||||
|
os.path.expanduser("~/Library/Application Support"), app_name
|
||||||
|
)
|
||||||
|
return os.path.join(
|
||||||
|
os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")),
|
||||||
|
_posixify(app_name),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PacifyFlushWrapper(object):
|
||||||
|
"""This wrapper is used to catch and suppress BrokenPipeErrors resulting
|
||||||
|
from ``.flush()`` being called on broken pipe during the shutdown/final-GC
|
||||||
|
of the Python interpreter. Notably ``.flush()`` is always called on
|
||||||
|
``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any
|
||||||
|
other cleanup code, and the case where the underlying file is not a broken
|
||||||
|
pipe, all calls and attributes are proxied.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, wrapped):
|
||||||
|
self.wrapped = wrapped
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
try:
|
||||||
|
self.wrapped.flush()
|
||||||
|
except IOError as e:
|
||||||
|
import errno
|
||||||
|
|
||||||
|
if e.errno != errno.EPIPE:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self.wrapped, attr)
|
||||||
46
mixly/tools/python/dotenv/__init__.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
from .compat import IS_TYPE_CHECKING
|
||||||
|
from .main import load_dotenv, get_key, set_key, unset_key, find_dotenv, dotenv_values
|
||||||
|
|
||||||
|
if IS_TYPE_CHECKING:
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
|
||||||
|
def load_ipython_extension(ipython):
|
||||||
|
# type: (Any) -> None
|
||||||
|
from .ipython import load_ipython_extension
|
||||||
|
load_ipython_extension(ipython)
|
||||||
|
|
||||||
|
|
||||||
|
def get_cli_string(path=None, action=None, key=None, value=None, quote=None):
|
||||||
|
# type: (Optional[str], Optional[str], Optional[str], Optional[str], Optional[str]) -> str
|
||||||
|
"""Returns a string suitable for running as a shell script.
|
||||||
|
|
||||||
|
Useful for converting a arguments passed to a fabric task
|
||||||
|
to be passed to a `local` or `run` command.
|
||||||
|
"""
|
||||||
|
command = ['dotenv']
|
||||||
|
if quote:
|
||||||
|
command.append('-q %s' % quote)
|
||||||
|
if path:
|
||||||
|
command.append('-f %s' % path)
|
||||||
|
if action:
|
||||||
|
command.append(action)
|
||||||
|
if key:
|
||||||
|
command.append(key)
|
||||||
|
if value:
|
||||||
|
if ' ' in value:
|
||||||
|
command.append('"%s"' % value)
|
||||||
|
else:
|
||||||
|
command.append(value)
|
||||||
|
|
||||||
|
return ' '.join(command).strip()
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['get_cli_string',
|
||||||
|
'load_dotenv',
|
||||||
|
'dotenv_values',
|
||||||
|
'get_key',
|
||||||
|
'set_key',
|
||||||
|
'unset_key',
|
||||||
|
'find_dotenv',
|
||||||
|
'load_ipython_extension']
|
||||||
174
mixly/tools/python/dotenv/cli.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from subprocess import Popen
|
||||||
|
|
||||||
|
try:
|
||||||
|
import click
|
||||||
|
except ImportError:
|
||||||
|
sys.stderr.write('It seems python-dotenv is not installed with cli option. \n'
|
||||||
|
'Run pip install "python-dotenv[cli]" to fix this.')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
from .compat import IS_TYPE_CHECKING, to_env
|
||||||
|
from .main import dotenv_values, get_key, set_key, unset_key
|
||||||
|
from .version import __version__
|
||||||
|
|
||||||
|
if IS_TYPE_CHECKING:
|
||||||
|
from typing import Any, List, Dict
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.option('-f', '--file', default=os.path.join(os.getcwd(), '.env'),
|
||||||
|
type=click.Path(file_okay=True),
|
||||||
|
help="Location of the .env file, defaults to .env file in current working directory.")
|
||||||
|
@click.option('-q', '--quote', default='always',
|
||||||
|
type=click.Choice(['always', 'never', 'auto']),
|
||||||
|
help="Whether to quote or not the variable values. Default mode is always. This does not affect parsing.")
|
||||||
|
@click.option('-e', '--export', default=False,
|
||||||
|
type=click.BOOL,
|
||||||
|
help="Whether to write the dot file as an executable bash script.")
|
||||||
|
@click.version_option(version=__version__)
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx, file, quote, export):
|
||||||
|
# type: (click.Context, Any, Any, Any) -> None
|
||||||
|
'''This script is used to set, get or unset values from a .env file.'''
|
||||||
|
ctx.obj = {}
|
||||||
|
ctx.obj['QUOTE'] = quote
|
||||||
|
ctx.obj['EXPORT'] = export
|
||||||
|
ctx.obj['FILE'] = file
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_context
|
||||||
|
def list(ctx):
|
||||||
|
# type: (click.Context) -> None
|
||||||
|
'''Display all the stored key/value.'''
|
||||||
|
file = ctx.obj['FILE']
|
||||||
|
if not os.path.isfile(file):
|
||||||
|
raise click.BadParameter(
|
||||||
|
'Path "%s" does not exist.' % (file),
|
||||||
|
ctx=ctx
|
||||||
|
)
|
||||||
|
dotenv_as_dict = dotenv_values(file)
|
||||||
|
for k, v in dotenv_as_dict.items():
|
||||||
|
click.echo('%s=%s' % (k, v))
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_context
|
||||||
|
@click.argument('key', required=True)
|
||||||
|
@click.argument('value', required=True)
|
||||||
|
def set(ctx, key, value):
|
||||||
|
# type: (click.Context, Any, Any) -> None
|
||||||
|
'''Store the given key/value.'''
|
||||||
|
file = ctx.obj['FILE']
|
||||||
|
quote = ctx.obj['QUOTE']
|
||||||
|
export = ctx.obj['EXPORT']
|
||||||
|
success, key, value = set_key(file, key, value, quote, export)
|
||||||
|
if success:
|
||||||
|
click.echo('%s=%s' % (key, value))
|
||||||
|
else:
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_context
|
||||||
|
@click.argument('key', required=True)
|
||||||
|
def get(ctx, key):
|
||||||
|
# type: (click.Context, Any) -> None
|
||||||
|
'''Retrieve the value for the given key.'''
|
||||||
|
file = ctx.obj['FILE']
|
||||||
|
if not os.path.isfile(file):
|
||||||
|
raise click.BadParameter(
|
||||||
|
'Path "%s" does not exist.' % (file),
|
||||||
|
ctx=ctx
|
||||||
|
)
|
||||||
|
stored_value = get_key(file, key)
|
||||||
|
if stored_value:
|
||||||
|
click.echo(stored_value)
|
||||||
|
else:
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.pass_context
|
||||||
|
@click.argument('key', required=True)
|
||||||
|
def unset(ctx, key):
|
||||||
|
# type: (click.Context, Any) -> None
|
||||||
|
'''Removes the given key.'''
|
||||||
|
file = ctx.obj['FILE']
|
||||||
|
quote = ctx.obj['QUOTE']
|
||||||
|
success, key = unset_key(file, key, quote)
|
||||||
|
if success:
|
||||||
|
click.echo("Successfully removed %s" % key)
|
||||||
|
else:
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(context_settings={'ignore_unknown_options': True})
|
||||||
|
@click.pass_context
|
||||||
|
@click.option(
|
||||||
|
"--override/--no-override",
|
||||||
|
default=True,
|
||||||
|
help="Override variables from the environment file with those from the .env file.",
|
||||||
|
)
|
||||||
|
@click.argument('commandline', nargs=-1, type=click.UNPROCESSED)
|
||||||
|
def run(ctx, override, commandline):
|
||||||
|
# type: (click.Context, bool, List[str]) -> None
|
||||||
|
"""Run command with environment variables present."""
|
||||||
|
file = ctx.obj['FILE']
|
||||||
|
if not os.path.isfile(file):
|
||||||
|
raise click.BadParameter(
|
||||||
|
'Invalid value for \'-f\' "%s" does not exist.' % (file),
|
||||||
|
ctx=ctx
|
||||||
|
)
|
||||||
|
dotenv_as_dict = {
|
||||||
|
to_env(k): to_env(v)
|
||||||
|
for (k, v) in dotenv_values(file).items()
|
||||||
|
if v is not None and (override or to_env(k) not in os.environ)
|
||||||
|
}
|
||||||
|
|
||||||
|
if not commandline:
|
||||||
|
click.echo('No command given.')
|
||||||
|
exit(1)
|
||||||
|
ret = run_command(commandline, dotenv_as_dict)
|
||||||
|
exit(ret)
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(command, env):
|
||||||
|
# type: (List[str], Dict[str, str]) -> int
|
||||||
|
"""Run command in sub process.
|
||||||
|
|
||||||
|
Runs the command in a sub process with the variables from `env`
|
||||||
|
added in the current environment variables.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
command: List[str]
|
||||||
|
The command and it's parameters
|
||||||
|
env: Dict
|
||||||
|
The additional environment variables
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
The return code of the command
|
||||||
|
|
||||||
|
"""
|
||||||
|
# copy the current environment variables and add the vales from
|
||||||
|
# `env`
|
||||||
|
cmd_env = os.environ.copy()
|
||||||
|
cmd_env.update(env)
|
||||||
|
|
||||||
|
p = Popen(command,
|
||||||
|
universal_newlines=True,
|
||||||
|
bufsize=0,
|
||||||
|
shell=False,
|
||||||
|
env=cmd_env)
|
||||||
|
_, _ = p.communicate()
|
||||||
|
|
||||||
|
return p.returncode
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cli()
|
||||||
49
mixly/tools/python/dotenv/compat.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
PY2 = sys.version_info[0] == 2 # type: bool
|
||||||
|
|
||||||
|
if PY2:
|
||||||
|
from StringIO import StringIO # noqa
|
||||||
|
else:
|
||||||
|
from io import StringIO # noqa
|
||||||
|
|
||||||
|
|
||||||
|
def is_type_checking():
|
||||||
|
# type: () -> bool
|
||||||
|
try:
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
return TYPE_CHECKING
|
||||||
|
|
||||||
|
|
||||||
|
IS_TYPE_CHECKING = is_type_checking()
|
||||||
|
|
||||||
|
|
||||||
|
if IS_TYPE_CHECKING:
|
||||||
|
from typing import Text
|
||||||
|
|
||||||
|
|
||||||
|
def to_env(text):
|
||||||
|
# type: (Text) -> str
|
||||||
|
"""
|
||||||
|
Encode a string the same way whether it comes from the environment or a `.env` file.
|
||||||
|
"""
|
||||||
|
if PY2:
|
||||||
|
return text.encode(sys.getfilesystemencoding() or "utf-8")
|
||||||
|
else:
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def to_text(string):
|
||||||
|
# type: (str) -> Text
|
||||||
|
"""
|
||||||
|
Make a string Unicode if it isn't already.
|
||||||
|
|
||||||
|
This is useful for defining raw unicode strings because `ur"foo"` isn't valid in
|
||||||
|
Python 3.
|
||||||
|
"""
|
||||||
|
if PY2:
|
||||||
|
return string.decode("utf-8")
|
||||||
|
else:
|
||||||
|
return string
|
||||||
41
mixly/tools/python/dotenv/ipython.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
from IPython.core.magic import Magics, line_magic, magics_class # type: ignore
|
||||||
|
from IPython.core.magic_arguments import (argument, magic_arguments, # type: ignore
|
||||||
|
parse_argstring) # type: ignore
|
||||||
|
|
||||||
|
from .main import find_dotenv, load_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
@magics_class
|
||||||
|
class IPythonDotEnv(Magics):
|
||||||
|
|
||||||
|
@magic_arguments()
|
||||||
|
@argument(
|
||||||
|
'-o', '--override', action='store_true',
|
||||||
|
help="Indicate to override existing variables"
|
||||||
|
)
|
||||||
|
@argument(
|
||||||
|
'-v', '--verbose', action='store_true',
|
||||||
|
help="Indicate function calls to be verbose"
|
||||||
|
)
|
||||||
|
@argument('dotenv_path', nargs='?', type=str, default='.env',
|
||||||
|
help='Search in increasingly higher folders for the `dotenv_path`')
|
||||||
|
@line_magic
|
||||||
|
def dotenv(self, line):
|
||||||
|
args = parse_argstring(self.dotenv, line)
|
||||||
|
# Locate the .env file
|
||||||
|
dotenv_path = args.dotenv_path
|
||||||
|
try:
|
||||||
|
dotenv_path = find_dotenv(dotenv_path, True, True)
|
||||||
|
except IOError:
|
||||||
|
print("cannot find .env file")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Load the .env file
|
||||||
|
load_dotenv(dotenv_path, verbose=args.verbose, override=args.override)
|
||||||
|
|
||||||
|
|
||||||
|
def load_ipython_extension(ipython):
|
||||||
|
"""Register the %dotenv magic."""
|
||||||
|
ipython.register_magics(IPythonDotEnv)
|
||||||
355
mixly/tools/python/dotenv/main.py
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import absolute_import, print_function, unicode_literals
|
||||||
|
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from collections import OrderedDict
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from .compat import IS_TYPE_CHECKING, PY2, StringIO, to_env
|
||||||
|
from .parser import Binding, parse_stream
|
||||||
|
from .variables import parse_variables
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
if IS_TYPE_CHECKING:
|
||||||
|
from typing import (IO, Dict, Iterable, Iterator, Mapping, Optional, Text,
|
||||||
|
Tuple, Union)
|
||||||
|
if sys.version_info >= (3, 6):
|
||||||
|
_PathLike = os.PathLike
|
||||||
|
else:
|
||||||
|
_PathLike = Text
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 0):
|
||||||
|
_StringIO = StringIO
|
||||||
|
else:
|
||||||
|
_StringIO = StringIO[Text]
|
||||||
|
|
||||||
|
|
||||||
|
def with_warn_for_invalid_lines(mappings):
|
||||||
|
# type: (Iterator[Binding]) -> Iterator[Binding]
|
||||||
|
for mapping in mappings:
|
||||||
|
if mapping.error:
|
||||||
|
logger.warning(
|
||||||
|
"Python-dotenv could not parse statement starting at line %s",
|
||||||
|
mapping.original.line,
|
||||||
|
)
|
||||||
|
yield mapping
|
||||||
|
|
||||||
|
|
||||||
|
class DotEnv():
|
||||||
|
|
||||||
|
def __init__(self, dotenv_path, verbose=False, encoding=None, interpolate=True, override=True):
|
||||||
|
# type: (Union[Text, _PathLike, _StringIO], bool, Union[None, Text], bool, bool) -> None
|
||||||
|
self.dotenv_path = dotenv_path # type: Union[Text,_PathLike, _StringIO]
|
||||||
|
self._dict = None # type: Optional[Dict[Text, Optional[Text]]]
|
||||||
|
self.verbose = verbose # type: bool
|
||||||
|
self.encoding = encoding # type: Union[None, Text]
|
||||||
|
self.interpolate = interpolate # type: bool
|
||||||
|
self.override = override # type: bool
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _get_stream(self):
|
||||||
|
# type: () -> Iterator[IO[Text]]
|
||||||
|
if isinstance(self.dotenv_path, StringIO):
|
||||||
|
yield self.dotenv_path
|
||||||
|
elif os.path.isfile(self.dotenv_path):
|
||||||
|
with io.open(self.dotenv_path, encoding=self.encoding) as stream:
|
||||||
|
yield stream
|
||||||
|
else:
|
||||||
|
if self.verbose:
|
||||||
|
logger.info("Python-dotenv could not find configuration file %s.", self.dotenv_path or '.env')
|
||||||
|
yield StringIO('')
|
||||||
|
|
||||||
|
def dict(self):
|
||||||
|
# type: () -> Dict[Text, Optional[Text]]
|
||||||
|
"""Return dotenv as dict"""
|
||||||
|
if self._dict:
|
||||||
|
return self._dict
|
||||||
|
|
||||||
|
raw_values = self.parse()
|
||||||
|
|
||||||
|
if self.interpolate:
|
||||||
|
self._dict = OrderedDict(resolve_variables(raw_values, override=self.override))
|
||||||
|
else:
|
||||||
|
self._dict = OrderedDict(raw_values)
|
||||||
|
|
||||||
|
return self._dict
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
# type: () -> Iterator[Tuple[Text, Optional[Text]]]
|
||||||
|
with self._get_stream() as stream:
|
||||||
|
for mapping in with_warn_for_invalid_lines(parse_stream(stream)):
|
||||||
|
if mapping.key is not None:
|
||||||
|
yield mapping.key, mapping.value
|
||||||
|
|
||||||
|
def set_as_environment_variables(self):
|
||||||
|
# type: () -> bool
|
||||||
|
"""
|
||||||
|
Load the current dotenv as system environment variable.
|
||||||
|
"""
|
||||||
|
for k, v in self.dict().items():
|
||||||
|
if k in os.environ and not self.override:
|
||||||
|
continue
|
||||||
|
if v is not None:
|
||||||
|
os.environ[to_env(k)] = to_env(v)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
# type: (Text) -> Optional[Text]
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
data = self.dict()
|
||||||
|
|
||||||
|
if key in data:
|
||||||
|
return data[key]
|
||||||
|
|
||||||
|
if self.verbose:
|
||||||
|
logger.warning("Key %s not found in %s.", key, self.dotenv_path)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_key(dotenv_path, key_to_get):
|
||||||
|
# type: (Union[Text, _PathLike], Text) -> Optional[Text]
|
||||||
|
"""
|
||||||
|
Gets the value of a given key from the given .env
|
||||||
|
|
||||||
|
If the .env path given doesn't exist, fails
|
||||||
|
"""
|
||||||
|
return DotEnv(dotenv_path, verbose=True).get(key_to_get)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def rewrite(path):
|
||||||
|
# type: (_PathLike) -> Iterator[Tuple[IO[Text], IO[Text]]]
|
||||||
|
try:
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
with io.open(path, "w+") as source:
|
||||||
|
source.write("")
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w+", delete=False) as dest:
|
||||||
|
with io.open(path) as source:
|
||||||
|
yield (source, dest) # type: ignore
|
||||||
|
except BaseException:
|
||||||
|
if os.path.isfile(dest.name):
|
||||||
|
os.unlink(dest.name)
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
shutil.move(dest.name, path)
|
||||||
|
|
||||||
|
|
||||||
|
def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always", export=False):
|
||||||
|
# type: (_PathLike, Text, Text, Text, bool) -> Tuple[Optional[bool], Text, Text]
|
||||||
|
"""
|
||||||
|
Adds or Updates a key/value to the given .env
|
||||||
|
|
||||||
|
If the .env path given doesn't exist, fails instead of risking creating
|
||||||
|
an orphan .env somewhere in the filesystem
|
||||||
|
"""
|
||||||
|
value_to_set = value_to_set.strip("'").strip('"')
|
||||||
|
|
||||||
|
if " " in value_to_set:
|
||||||
|
quote_mode = "always"
|
||||||
|
|
||||||
|
if quote_mode == "always":
|
||||||
|
value_out = '"{}"'.format(value_to_set.replace('"', '\\"'))
|
||||||
|
else:
|
||||||
|
value_out = value_to_set
|
||||||
|
if export:
|
||||||
|
line_out = 'export {}={}\n'.format(key_to_set, value_out)
|
||||||
|
else:
|
||||||
|
line_out = "{}={}\n".format(key_to_set, value_out)
|
||||||
|
|
||||||
|
with rewrite(dotenv_path) as (source, dest):
|
||||||
|
replaced = False
|
||||||
|
for mapping in with_warn_for_invalid_lines(parse_stream(source)):
|
||||||
|
if mapping.key == key_to_set:
|
||||||
|
dest.write(line_out)
|
||||||
|
replaced = True
|
||||||
|
else:
|
||||||
|
dest.write(mapping.original.string)
|
||||||
|
if not replaced:
|
||||||
|
dest.write(line_out)
|
||||||
|
|
||||||
|
return True, key_to_set, value_to_set
|
||||||
|
|
||||||
|
|
||||||
|
def unset_key(dotenv_path, key_to_unset, quote_mode="always"):
|
||||||
|
# type: (_PathLike, Text, Text) -> Tuple[Optional[bool], Text]
|
||||||
|
"""
|
||||||
|
Removes a given key from the given .env
|
||||||
|
|
||||||
|
If the .env path given doesn't exist, fails
|
||||||
|
If the given key doesn't exist in the .env, fails
|
||||||
|
"""
|
||||||
|
if not os.path.exists(dotenv_path):
|
||||||
|
logger.warning("Can't delete from %s - it doesn't exist.", dotenv_path)
|
||||||
|
return None, key_to_unset
|
||||||
|
|
||||||
|
removed = False
|
||||||
|
with rewrite(dotenv_path) as (source, dest):
|
||||||
|
for mapping in with_warn_for_invalid_lines(parse_stream(source)):
|
||||||
|
if mapping.key == key_to_unset:
|
||||||
|
removed = True
|
||||||
|
else:
|
||||||
|
dest.write(mapping.original.string)
|
||||||
|
|
||||||
|
if not removed:
|
||||||
|
logger.warning("Key %s not removed from %s - key doesn't exist.", key_to_unset, dotenv_path)
|
||||||
|
return None, key_to_unset
|
||||||
|
|
||||||
|
return removed, key_to_unset
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_variables(values, override):
|
||||||
|
# type: (Iterable[Tuple[Text, Optional[Text]]], bool) -> Mapping[Text, Optional[Text]]
|
||||||
|
|
||||||
|
new_values = {} # type: Dict[Text, Optional[Text]]
|
||||||
|
|
||||||
|
for (name, value) in values:
|
||||||
|
if value is None:
|
||||||
|
result = None
|
||||||
|
else:
|
||||||
|
atoms = parse_variables(value)
|
||||||
|
env = {} # type: Dict[Text, Optional[Text]]
|
||||||
|
if override:
|
||||||
|
env.update(os.environ) # type: ignore
|
||||||
|
env.update(new_values)
|
||||||
|
else:
|
||||||
|
env.update(new_values)
|
||||||
|
env.update(os.environ) # type: ignore
|
||||||
|
result = "".join(atom.resolve(env) for atom in atoms)
|
||||||
|
|
||||||
|
new_values[name] = result
|
||||||
|
|
||||||
|
return new_values
|
||||||
|
|
||||||
|
|
||||||
|
def _walk_to_root(path):
|
||||||
|
# type: (Text) -> Iterator[Text]
|
||||||
|
"""
|
||||||
|
Yield directories starting from the given directory up to the root
|
||||||
|
"""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
raise IOError('Starting path not found')
|
||||||
|
|
||||||
|
if os.path.isfile(path):
|
||||||
|
path = os.path.dirname(path)
|
||||||
|
|
||||||
|
last_dir = None
|
||||||
|
current_dir = os.path.abspath(path)
|
||||||
|
while last_dir != current_dir:
|
||||||
|
yield current_dir
|
||||||
|
parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir))
|
||||||
|
last_dir, current_dir = current_dir, parent_dir
|
||||||
|
|
||||||
|
|
||||||
|
def find_dotenv(filename='.env', raise_error_if_not_found=False, usecwd=False):
|
||||||
|
# type: (Text, bool, bool) -> Text
|
||||||
|
"""
|
||||||
|
Search in increasingly higher folders for the given file
|
||||||
|
|
||||||
|
Returns path to the file if found, or an empty string otherwise
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _is_interactive():
|
||||||
|
""" Decide whether this is running in a REPL or IPython notebook """
|
||||||
|
main = __import__('__main__', None, None, fromlist=['__file__'])
|
||||||
|
return not hasattr(main, '__file__')
|
||||||
|
|
||||||
|
if usecwd or _is_interactive() or getattr(sys, 'frozen', False):
|
||||||
|
# Should work without __file__, e.g. in REPL or IPython notebook.
|
||||||
|
path = os.getcwd()
|
||||||
|
else:
|
||||||
|
# will work for .py files
|
||||||
|
frame = sys._getframe()
|
||||||
|
# find first frame that is outside of this file
|
||||||
|
if PY2 and not __file__.endswith('.py'):
|
||||||
|
# in Python2 __file__ extension could be .pyc or .pyo (this doesn't account
|
||||||
|
# for edge case of Python compiled for non-standard extension)
|
||||||
|
current_file = __file__.rsplit('.', 1)[0] + '.py'
|
||||||
|
else:
|
||||||
|
current_file = __file__
|
||||||
|
|
||||||
|
while frame.f_code.co_filename == current_file:
|
||||||
|
assert frame.f_back is not None
|
||||||
|
frame = frame.f_back
|
||||||
|
frame_filename = frame.f_code.co_filename
|
||||||
|
path = os.path.dirname(os.path.abspath(frame_filename))
|
||||||
|
|
||||||
|
for dirname in _walk_to_root(path):
|
||||||
|
check_path = os.path.join(dirname, filename)
|
||||||
|
if os.path.isfile(check_path):
|
||||||
|
return check_path
|
||||||
|
|
||||||
|
if raise_error_if_not_found:
|
||||||
|
raise IOError('File not found')
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def load_dotenv(
|
||||||
|
dotenv_path=None,
|
||||||
|
stream=None,
|
||||||
|
verbose=False,
|
||||||
|
override=False,
|
||||||
|
interpolate=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
):
|
||||||
|
# type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, bool, bool, Optional[Text]) -> bool # noqa
|
||||||
|
"""Parse a .env file and then load all the variables found as environment variables.
|
||||||
|
|
||||||
|
- *dotenv_path*: absolute or relative path to .env file.
|
||||||
|
- *stream*: `StringIO` object with .env content, used if `dotenv_path` is `None`.
|
||||||
|
- *verbose*: whether to output a warning the .env file is missing. Defaults to
|
||||||
|
`False`.
|
||||||
|
- *override*: whether to override the system environment variables with the variables
|
||||||
|
in `.env` file. Defaults to `False`.
|
||||||
|
- *encoding*: encoding to be used to read the file.
|
||||||
|
|
||||||
|
If both `dotenv_path` and `stream`, `find_dotenv()` is used to find the .env file.
|
||||||
|
"""
|
||||||
|
f = dotenv_path or stream or find_dotenv()
|
||||||
|
dotenv = DotEnv(
|
||||||
|
f,
|
||||||
|
verbose=verbose,
|
||||||
|
interpolate=interpolate,
|
||||||
|
override=override,
|
||||||
|
encoding=encoding,
|
||||||
|
)
|
||||||
|
return dotenv.set_as_environment_variables()
|
||||||
|
|
||||||
|
|
||||||
|
def dotenv_values(
|
||||||
|
dotenv_path=None,
|
||||||
|
stream=None,
|
||||||
|
verbose=False,
|
||||||
|
interpolate=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
):
|
||||||
|
# type: (Union[Text, _PathLike, None], Optional[_StringIO], bool, bool, Optional[Text]) -> Dict[Text, Optional[Text]] # noqa: E501
|
||||||
|
"""
|
||||||
|
Parse a .env file and return its content as a dict.
|
||||||
|
|
||||||
|
- *dotenv_path*: absolute or relative path to .env file.
|
||||||
|
- *stream*: `StringIO` object with .env content, used if `dotenv_path` is `None`.
|
||||||
|
- *verbose*: whether to output a warning the .env file is missing. Defaults to
|
||||||
|
`False`.
|
||||||
|
in `.env` file. Defaults to `False`.
|
||||||
|
- *encoding*: encoding to be used to read the file.
|
||||||
|
|
||||||
|
If both `dotenv_path` and `stream`, `find_dotenv()` is used to find the .env file.
|
||||||
|
"""
|
||||||
|
f = dotenv_path or stream or find_dotenv()
|
||||||
|
return DotEnv(
|
||||||
|
f,
|
||||||
|
verbose=verbose,
|
||||||
|
interpolate=interpolate,
|
||||||
|
override=True,
|
||||||
|
encoding=encoding,
|
||||||
|
).dict()
|
||||||
231
mixly/tools/python/dotenv/parser.py
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import codecs
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .compat import IS_TYPE_CHECKING, to_text
|
||||||
|
|
||||||
|
if IS_TYPE_CHECKING:
|
||||||
|
from typing import ( # noqa:F401
|
||||||
|
IO, Iterator, Match, NamedTuple, Optional, Pattern, Sequence, Text,
|
||||||
|
Tuple
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_regex(string, extra_flags=0):
|
||||||
|
# type: (str, int) -> Pattern[Text]
|
||||||
|
return re.compile(to_text(string), re.UNICODE | extra_flags)
|
||||||
|
|
||||||
|
|
||||||
|
_newline = make_regex(r"(\r\n|\n|\r)")
|
||||||
|
_multiline_whitespace = make_regex(r"\s*", extra_flags=re.MULTILINE)
|
||||||
|
_whitespace = make_regex(r"[^\S\r\n]*")
|
||||||
|
_export = make_regex(r"(?:export[^\S\r\n]+)?")
|
||||||
|
_single_quoted_key = make_regex(r"'([^']+)'")
|
||||||
|
_unquoted_key = make_regex(r"([^=\#\s]+)")
|
||||||
|
_equal_sign = make_regex(r"(=[^\S\r\n]*)")
|
||||||
|
_single_quoted_value = make_regex(r"'((?:\\'|[^'])*)'")
|
||||||
|
_double_quoted_value = make_regex(r'"((?:\\"|[^"])*)"')
|
||||||
|
_unquoted_value = make_regex(r"([^\r\n]*)")
|
||||||
|
_comment = make_regex(r"(?:[^\S\r\n]*#[^\r\n]*)?")
|
||||||
|
_end_of_line = make_regex(r"[^\S\r\n]*(?:\r\n|\n|\r|$)")
|
||||||
|
_rest_of_line = make_regex(r"[^\r\n]*(?:\r|\n|\r\n)?")
|
||||||
|
_double_quote_escapes = make_regex(r"\\[\\'\"abfnrtv]")
|
||||||
|
_single_quote_escapes = make_regex(r"\\[\\']")
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# this is necessary because we only import these from typing
|
||||||
|
# when we are type checking, and the linter is upset if we
|
||||||
|
# re-import
|
||||||
|
import typing
|
||||||
|
|
||||||
|
Original = typing.NamedTuple(
|
||||||
|
"Original",
|
||||||
|
[
|
||||||
|
("string", typing.Text),
|
||||||
|
("line", int),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
Binding = typing.NamedTuple(
|
||||||
|
"Binding",
|
||||||
|
[
|
||||||
|
("key", typing.Optional[typing.Text]),
|
||||||
|
("value", typing.Optional[typing.Text]),
|
||||||
|
("original", Original),
|
||||||
|
("error", bool),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
from collections import namedtuple
|
||||||
|
Original = namedtuple( # type: ignore
|
||||||
|
"Original",
|
||||||
|
[
|
||||||
|
"string",
|
||||||
|
"line",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
Binding = namedtuple( # type: ignore
|
||||||
|
"Binding",
|
||||||
|
[
|
||||||
|
"key",
|
||||||
|
"value",
|
||||||
|
"original",
|
||||||
|
"error",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Position:
|
||||||
|
def __init__(self, chars, line):
|
||||||
|
# type: (int, int) -> None
|
||||||
|
self.chars = chars
|
||||||
|
self.line = line
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def start(cls):
|
||||||
|
# type: () -> Position
|
||||||
|
return cls(chars=0, line=1)
|
||||||
|
|
||||||
|
def set(self, other):
|
||||||
|
# type: (Position) -> None
|
||||||
|
self.chars = other.chars
|
||||||
|
self.line = other.line
|
||||||
|
|
||||||
|
def advance(self, string):
|
||||||
|
# type: (Text) -> None
|
||||||
|
self.chars += len(string)
|
||||||
|
self.line += len(re.findall(_newline, string))
|
||||||
|
|
||||||
|
|
||||||
|
class Error(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Reader:
|
||||||
|
def __init__(self, stream):
|
||||||
|
# type: (IO[Text]) -> None
|
||||||
|
self.string = stream.read()
|
||||||
|
self.position = Position.start()
|
||||||
|
self.mark = Position.start()
|
||||||
|
|
||||||
|
def has_next(self):
|
||||||
|
# type: () -> bool
|
||||||
|
return self.position.chars < len(self.string)
|
||||||
|
|
||||||
|
def set_mark(self):
|
||||||
|
# type: () -> None
|
||||||
|
self.mark.set(self.position)
|
||||||
|
|
||||||
|
def get_marked(self):
|
||||||
|
# type: () -> Original
|
||||||
|
return Original(
|
||||||
|
string=self.string[self.mark.chars:self.position.chars],
|
||||||
|
line=self.mark.line,
|
||||||
|
)
|
||||||
|
|
||||||
|
def peek(self, count):
|
||||||
|
# type: (int) -> Text
|
||||||
|
return self.string[self.position.chars:self.position.chars + count]
|
||||||
|
|
||||||
|
def read(self, count):
|
||||||
|
# type: (int) -> Text
|
||||||
|
result = self.string[self.position.chars:self.position.chars + count]
|
||||||
|
if len(result) < count:
|
||||||
|
raise Error("read: End of string")
|
||||||
|
self.position.advance(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def read_regex(self, regex):
|
||||||
|
# type: (Pattern[Text]) -> Sequence[Text]
|
||||||
|
match = regex.match(self.string, self.position.chars)
|
||||||
|
if match is None:
|
||||||
|
raise Error("read_regex: Pattern not found")
|
||||||
|
self.position.advance(self.string[match.start():match.end()])
|
||||||
|
return match.groups()
|
||||||
|
|
||||||
|
|
||||||
|
def decode_escapes(regex, string):
|
||||||
|
# type: (Pattern[Text], Text) -> Text
|
||||||
|
def decode_match(match):
|
||||||
|
# type: (Match[Text]) -> Text
|
||||||
|
return codecs.decode(match.group(0), 'unicode-escape') # type: ignore
|
||||||
|
|
||||||
|
return regex.sub(decode_match, string)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_key(reader):
|
||||||
|
# type: (Reader) -> Optional[Text]
|
||||||
|
char = reader.peek(1)
|
||||||
|
if char == "#":
|
||||||
|
return None
|
||||||
|
elif char == "'":
|
||||||
|
(key,) = reader.read_regex(_single_quoted_key)
|
||||||
|
else:
|
||||||
|
(key,) = reader.read_regex(_unquoted_key)
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
def parse_unquoted_value(reader):
|
||||||
|
# type: (Reader) -> Text
|
||||||
|
(part,) = reader.read_regex(_unquoted_value)
|
||||||
|
return re.sub(r"\s+#.*", "", part).rstrip()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_value(reader):
|
||||||
|
# type: (Reader) -> Text
|
||||||
|
char = reader.peek(1)
|
||||||
|
if char == u"'":
|
||||||
|
(value,) = reader.read_regex(_single_quoted_value)
|
||||||
|
return decode_escapes(_single_quote_escapes, value)
|
||||||
|
elif char == u'"':
|
||||||
|
(value,) = reader.read_regex(_double_quoted_value)
|
||||||
|
return decode_escapes(_double_quote_escapes, value)
|
||||||
|
elif char in (u"", u"\n", u"\r"):
|
||||||
|
return u""
|
||||||
|
else:
|
||||||
|
return parse_unquoted_value(reader)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_binding(reader):
|
||||||
|
# type: (Reader) -> Binding
|
||||||
|
reader.set_mark()
|
||||||
|
try:
|
||||||
|
reader.read_regex(_multiline_whitespace)
|
||||||
|
if not reader.has_next():
|
||||||
|
return Binding(
|
||||||
|
key=None,
|
||||||
|
value=None,
|
||||||
|
original=reader.get_marked(),
|
||||||
|
error=False,
|
||||||
|
)
|
||||||
|
reader.read_regex(_export)
|
||||||
|
key = parse_key(reader)
|
||||||
|
reader.read_regex(_whitespace)
|
||||||
|
if reader.peek(1) == "=":
|
||||||
|
reader.read_regex(_equal_sign)
|
||||||
|
value = parse_value(reader) # type: Optional[Text]
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
reader.read_regex(_comment)
|
||||||
|
reader.read_regex(_end_of_line)
|
||||||
|
return Binding(
|
||||||
|
key=key,
|
||||||
|
value=value,
|
||||||
|
original=reader.get_marked(),
|
||||||
|
error=False,
|
||||||
|
)
|
||||||
|
except Error:
|
||||||
|
reader.read_regex(_rest_of_line)
|
||||||
|
return Binding(
|
||||||
|
key=None,
|
||||||
|
value=None,
|
||||||
|
original=reader.get_marked(),
|
||||||
|
error=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_stream(stream):
|
||||||
|
# type: (IO[Text]) -> Iterator[Binding]
|
||||||
|
reader = Reader(stream)
|
||||||
|
while reader.has_next():
|
||||||
|
yield parse_binding(reader)
|
||||||
1
mixly/tools/python/dotenv/py.typed
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Marker file for PEP 561
|
||||||
106
mixly/tools/python/dotenv/variables.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import re
|
||||||
|
from abc import ABCMeta
|
||||||
|
|
||||||
|
from .compat import IS_TYPE_CHECKING
|
||||||
|
|
||||||
|
if IS_TYPE_CHECKING:
|
||||||
|
from typing import Iterator, Mapping, Optional, Pattern, Text
|
||||||
|
|
||||||
|
|
||||||
|
_posix_variable = re.compile(
|
||||||
|
r"""
|
||||||
|
\$\{
|
||||||
|
(?P<name>[^\}:]*)
|
||||||
|
(?::-
|
||||||
|
(?P<default>[^\}]*)
|
||||||
|
)?
|
||||||
|
\}
|
||||||
|
""",
|
||||||
|
re.VERBOSE,
|
||||||
|
) # type: Pattern[Text]
|
||||||
|
|
||||||
|
|
||||||
|
class Atom():
|
||||||
|
__metaclass__ = ABCMeta
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
# type: (object) -> bool
|
||||||
|
result = self.__eq__(other)
|
||||||
|
if result is NotImplemented:
|
||||||
|
return NotImplemented
|
||||||
|
return not result
|
||||||
|
|
||||||
|
def resolve(self, env):
|
||||||
|
# type: (Mapping[Text, Optional[Text]]) -> Text
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class Literal(Atom):
|
||||||
|
def __init__(self, value):
|
||||||
|
# type: (Text) -> None
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
# type: () -> str
|
||||||
|
return "Literal(value={})".format(self.value)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
# type: (object) -> bool
|
||||||
|
if not isinstance(other, self.__class__):
|
||||||
|
return NotImplemented
|
||||||
|
return self.value == other.value
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
# type: () -> int
|
||||||
|
return hash((self.__class__, self.value))
|
||||||
|
|
||||||
|
def resolve(self, env):
|
||||||
|
# type: (Mapping[Text, Optional[Text]]) -> Text
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
class Variable(Atom):
|
||||||
|
def __init__(self, name, default):
|
||||||
|
# type: (Text, Optional[Text]) -> None
|
||||||
|
self.name = name
|
||||||
|
self.default = default
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
# type: () -> str
|
||||||
|
return "Variable(name={}, default={})".format(self.name, self.default)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
# type: (object) -> bool
|
||||||
|
if not isinstance(other, self.__class__):
|
||||||
|
return NotImplemented
|
||||||
|
return (self.name, self.default) == (other.name, other.default)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
# type: () -> int
|
||||||
|
return hash((self.__class__, self.name, self.default))
|
||||||
|
|
||||||
|
def resolve(self, env):
|
||||||
|
# type: (Mapping[Text, Optional[Text]]) -> Text
|
||||||
|
default = self.default if self.default is not None else ""
|
||||||
|
result = env.get(self.name, default)
|
||||||
|
return result if result is not None else ""
|
||||||
|
|
||||||
|
|
||||||
|
def parse_variables(value):
|
||||||
|
# type: (Text) -> Iterator[Atom]
|
||||||
|
cursor = 0
|
||||||
|
|
||||||
|
for match in _posix_variable.finditer(value):
|
||||||
|
(start, end) = match.span()
|
||||||
|
name = match.groupdict()["name"]
|
||||||
|
default = match.groupdict()["default"]
|
||||||
|
|
||||||
|
if start > cursor:
|
||||||
|
yield Literal(value=value[cursor:start])
|
||||||
|
|
||||||
|
yield Variable(name=name, default=default)
|
||||||
|
cursor = end
|
||||||
|
|
||||||
|
length = len(value)
|
||||||
|
if cursor < length:
|
||||||
|
yield Literal(value=value[cursor:length])
|
||||||
1
mixly/tools/python/dotenv/version.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__version__ = "0.17.1"
|
||||||
1337
mixly/tools/python/esptool/__init__.py
Normal file
9
mixly/tools/python/esptool/__main__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
|
||||||
|
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import esptool
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
esptool._main()
|
||||||
1380
mixly/tools/python/esptool/bin_image.py
Normal file
1474
mixly/tools/python/esptool/cmds.py
Normal file
93
mixly/tools/python/esptool/config.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2014-2023 Espressif Systems (Shanghai) CO LTD,
|
||||||
|
# other contributors as noted.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
import os
|
||||||
|
|
||||||
|
CONFIG_OPTIONS = [
|
||||||
|
"timeout",
|
||||||
|
"chip_erase_timeout",
|
||||||
|
"max_timeout",
|
||||||
|
"sync_timeout",
|
||||||
|
"md5_timeout_per_mb",
|
||||||
|
"erase_region_timeout_per_mb",
|
||||||
|
"erase_write_timeout_per_mb",
|
||||||
|
"mem_end_rom_timeout",
|
||||||
|
"serial_write_timeout",
|
||||||
|
"connect_attempts",
|
||||||
|
"write_block_attempts",
|
||||||
|
"reset_delay",
|
||||||
|
"open_port_attempts",
|
||||||
|
"custom_reset_sequence",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_config_file(file_path, verbose=False):
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
return False
|
||||||
|
|
||||||
|
cfg = configparser.RawConfigParser()
|
||||||
|
try:
|
||||||
|
cfg.read(file_path, encoding="UTF-8")
|
||||||
|
# Only consider it a valid config file if it contains [esptool] section
|
||||||
|
if cfg.has_section("esptool"):
|
||||||
|
if verbose:
|
||||||
|
unknown_opts = list(set(cfg.options("esptool")) - set(CONFIG_OPTIONS))
|
||||||
|
unknown_opts.sort()
|
||||||
|
no_of_unknown_opts = len(unknown_opts)
|
||||||
|
if no_of_unknown_opts > 0:
|
||||||
|
suffix = "s" if no_of_unknown_opts > 1 else ""
|
||||||
|
print(
|
||||||
|
"Ignoring unknown config file option{}: {}".format(
|
||||||
|
suffix, ", ".join(unknown_opts)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except (UnicodeDecodeError, configparser.Error) as e:
|
||||||
|
if verbose:
|
||||||
|
print(f"Ignoring invalid config file {file_path}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _find_config_file(dir_path, verbose=False):
|
||||||
|
for candidate in ("esptool.cfg", "setup.cfg", "tox.ini"):
|
||||||
|
cfg_path = os.path.join(dir_path, candidate)
|
||||||
|
if _validate_config_file(cfg_path, verbose):
|
||||||
|
return cfg_path
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def load_config_file(verbose=False):
|
||||||
|
set_with_env_var = False
|
||||||
|
env_var_path = os.environ.get("ESPTOOL_CFGFILE")
|
||||||
|
if env_var_path is not None and _validate_config_file(env_var_path):
|
||||||
|
cfg_file_path = env_var_path
|
||||||
|
set_with_env_var = True
|
||||||
|
else:
|
||||||
|
home_dir = os.path.expanduser("~")
|
||||||
|
os_config_dir = (
|
||||||
|
f"{home_dir}/.config/esptool"
|
||||||
|
if os.name == "posix"
|
||||||
|
else f"{home_dir}/AppData/Local/esptool/"
|
||||||
|
)
|
||||||
|
# Search priority: 1) current dir, 2) OS specific config dir, 3) home dir
|
||||||
|
for dir_path in (os.getcwd(), os_config_dir, home_dir):
|
||||||
|
cfg_file_path = _find_config_file(dir_path, verbose)
|
||||||
|
if cfg_file_path:
|
||||||
|
break
|
||||||
|
|
||||||
|
cfg = configparser.ConfigParser()
|
||||||
|
cfg["esptool"] = {} # Create an empty esptool config for when no file is found
|
||||||
|
|
||||||
|
if cfg_file_path is not None:
|
||||||
|
# If config file is found and validated, read and parse it
|
||||||
|
cfg.read(cfg_file_path)
|
||||||
|
if verbose:
|
||||||
|
msg = " (set with ESPTOOL_CFGFILE)" if set_with_env_var else ""
|
||||||
|
print(
|
||||||
|
f"Loaded custom configuration from "
|
||||||
|
f"{os.path.abspath(cfg_file_path)}{msg}"
|
||||||
|
)
|
||||||
|
return cfg, cfg_file_path
|
||||||
1719
mixly/tools/python/esptool/loader.py
Normal file
209
mixly/tools/python/esptool/reset.py
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2014-2023 Fredrik Ahlberg, Angus Gratton,
|
||||||
|
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
|
||||||
|
from .util import FatalError, PrintOnce
|
||||||
|
|
||||||
|
# Used for resetting into bootloader on Unix-like systems
|
||||||
|
if os.name != "nt":
|
||||||
|
import fcntl
|
||||||
|
import termios
|
||||||
|
|
||||||
|
# Constants used for terminal status lines reading/setting.
|
||||||
|
# Taken from pySerial's backend for IO:
|
||||||
|
# https://github.com/pyserial/pyserial/blob/master/serial/serialposix.py
|
||||||
|
TIOCMSET = getattr(termios, "TIOCMSET", 0x5418)
|
||||||
|
TIOCMGET = getattr(termios, "TIOCMGET", 0x5415)
|
||||||
|
TIOCM_DTR = getattr(termios, "TIOCM_DTR", 0x002)
|
||||||
|
TIOCM_RTS = getattr(termios, "TIOCM_RTS", 0x004)
|
||||||
|
|
||||||
|
DEFAULT_RESET_DELAY = 0.05 # default time to wait before releasing boot pin after reset
|
||||||
|
|
||||||
|
|
||||||
|
class ResetStrategy(object):
|
||||||
|
print_once = PrintOnce()
|
||||||
|
|
||||||
|
def __init__(self, port, reset_delay=DEFAULT_RESET_DELAY):
|
||||||
|
self.port = port
|
||||||
|
self.reset_delay = reset_delay
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
"""
|
||||||
|
On targets with USB modes, the reset process can cause the port to
|
||||||
|
disconnect / reconnect during reset.
|
||||||
|
This will retry reconnections on ports that
|
||||||
|
drop out during the reset sequence.
|
||||||
|
"""
|
||||||
|
for retry in reversed(range(3)):
|
||||||
|
try:
|
||||||
|
if not self.port.isOpen():
|
||||||
|
self.port.open()
|
||||||
|
self.reset()
|
||||||
|
break
|
||||||
|
except OSError as e:
|
||||||
|
# ENOTTY for TIOCMSET; EINVAL for TIOCMGET
|
||||||
|
if e.errno in [errno.ENOTTY, errno.EINVAL]:
|
||||||
|
self.print_once(
|
||||||
|
"WARNING: Chip was NOT reset. Setting RTS/DTR lines is not "
|
||||||
|
f"supported for port '{self.port.name}'. Set --before and --after "
|
||||||
|
"arguments to 'no_reset' and switch to bootloader manually to "
|
||||||
|
"avoid this warning."
|
||||||
|
)
|
||||||
|
break
|
||||||
|
elif not retry:
|
||||||
|
raise
|
||||||
|
self.port.close()
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _setDTR(self, state):
|
||||||
|
self.port.setDTR(state)
|
||||||
|
|
||||||
|
def _setRTS(self, state):
|
||||||
|
self.port.setRTS(state)
|
||||||
|
# Work-around for adapters on Windows using the usbser.sys driver:
|
||||||
|
# generate a dummy change to DTR so that the set-control-line-state
|
||||||
|
# request is sent with the updated RTS state and the same DTR state
|
||||||
|
self.port.setDTR(self.port.dtr)
|
||||||
|
|
||||||
|
def _setDTRandRTS(self, dtr=False, rts=False):
|
||||||
|
status = struct.unpack(
|
||||||
|
"I", fcntl.ioctl(self.port.fileno(), TIOCMGET, struct.pack("I", 0))
|
||||||
|
)[0]
|
||||||
|
if dtr:
|
||||||
|
status |= TIOCM_DTR
|
||||||
|
else:
|
||||||
|
status &= ~TIOCM_DTR
|
||||||
|
if rts:
|
||||||
|
status |= TIOCM_RTS
|
||||||
|
else:
|
||||||
|
status &= ~TIOCM_RTS
|
||||||
|
fcntl.ioctl(self.port.fileno(), TIOCMSET, struct.pack("I", status))
|
||||||
|
|
||||||
|
|
||||||
|
class ClassicReset(ResetStrategy):
|
||||||
|
"""
|
||||||
|
Classic reset sequence, sets DTR and RTS lines sequentially.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self._setDTR(False) # IO0=HIGH
|
||||||
|
self._setRTS(True) # EN=LOW, chip in reset
|
||||||
|
time.sleep(0.1)
|
||||||
|
self._setDTR(True) # IO0=LOW
|
||||||
|
self._setRTS(False) # EN=HIGH, chip out of reset
|
||||||
|
time.sleep(self.reset_delay)
|
||||||
|
self._setDTR(False) # IO0=HIGH, done
|
||||||
|
|
||||||
|
|
||||||
|
class UnixTightReset(ResetStrategy):
|
||||||
|
"""
|
||||||
|
UNIX-only reset sequence with custom implementation,
|
||||||
|
which allows setting DTR and RTS lines at the same time.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self._setDTRandRTS(False, False)
|
||||||
|
self._setDTRandRTS(True, True)
|
||||||
|
self._setDTRandRTS(False, True) # IO0=HIGH & EN=LOW, chip in reset
|
||||||
|
time.sleep(0.1)
|
||||||
|
self._setDTRandRTS(True, False) # IO0=LOW & EN=HIGH, chip out of reset
|
||||||
|
time.sleep(self.reset_delay)
|
||||||
|
self._setDTRandRTS(False, False) # IO0=HIGH, done
|
||||||
|
self._setDTR(False) # Needed in some environments to ensure IO0=HIGH
|
||||||
|
|
||||||
|
|
||||||
|
class USBJTAGSerialReset(ResetStrategy):
|
||||||
|
"""
|
||||||
|
Custom reset sequence, which is required when the device
|
||||||
|
is connecting via its USB-JTAG-Serial peripheral.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self._setRTS(False)
|
||||||
|
self._setDTR(False) # Idle
|
||||||
|
time.sleep(0.1)
|
||||||
|
self._setDTR(True) # Set IO0
|
||||||
|
self._setRTS(False)
|
||||||
|
time.sleep(0.1)
|
||||||
|
self._setRTS(True) # Reset. Calls inverted to go through (1,1) instead of (0,0)
|
||||||
|
self._setDTR(False)
|
||||||
|
self._setRTS(True) # RTS set as Windows only propagates DTR on RTS setting
|
||||||
|
time.sleep(0.1)
|
||||||
|
self._setDTR(False)
|
||||||
|
self._setRTS(False) # Chip out of reset
|
||||||
|
|
||||||
|
|
||||||
|
class HardReset(ResetStrategy):
|
||||||
|
"""
|
||||||
|
Reset sequence for hard resetting the chip.
|
||||||
|
Can be used to reset out of the bootloader or to restart a running app.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, port, uses_usb=False):
|
||||||
|
super().__init__(port)
|
||||||
|
self.uses_usb = uses_usb
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self._setRTS(True) # EN->LOW
|
||||||
|
if self.uses_usb:
|
||||||
|
# Give the chip some time to come out of reset,
|
||||||
|
# to be able to handle further DTR/RTS transitions
|
||||||
|
time.sleep(0.2)
|
||||||
|
self._setRTS(False)
|
||||||
|
time.sleep(0.2)
|
||||||
|
else:
|
||||||
|
time.sleep(0.1)
|
||||||
|
self._setRTS(False)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomReset(ResetStrategy):
|
||||||
|
"""
|
||||||
|
Custom reset strategy defined with a string.
|
||||||
|
|
||||||
|
CustomReset object is created as "rst = CustomReset(port, seq_str)"
|
||||||
|
and can be later executed simply with "rst()"
|
||||||
|
|
||||||
|
The seq_str input string consists of individual commands divided by "|".
|
||||||
|
Commands (e.g. R0) are defined by a code (R) and an argument (0).
|
||||||
|
|
||||||
|
The commands are:
|
||||||
|
D: setDTR - 1=True / 0=False
|
||||||
|
R: setRTS - 1=True / 0=False
|
||||||
|
U: setDTRandRTS (Unix-only) - 0,0 / 0,1 / 1,0 / or 1,1
|
||||||
|
W: Wait (time delay) - positive float number
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
"D0|R1|W0.1|D1|R0|W0.05|D0" represents the ClassicReset strategy
|
||||||
|
"U1,1|U0,1|W0.1|U1,0|W0.05|U0,0" represents the UnixTightReset strategy
|
||||||
|
"""
|
||||||
|
|
||||||
|
format_dict = {
|
||||||
|
"D": "self.port.setDTR({})",
|
||||||
|
"R": "self.port.setRTS({})",
|
||||||
|
"W": "time.sleep({})",
|
||||||
|
"U": "self._setDTRandRTS({})",
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
exec(self.constructed_strategy)
|
||||||
|
|
||||||
|
def __init__(self, port, seq_str):
|
||||||
|
super().__init__(port)
|
||||||
|
self.constructed_strategy = self._parse_string_to_seq(seq_str)
|
||||||
|
|
||||||
|
def _parse_string_to_seq(self, seq_str):
|
||||||
|
try:
|
||||||
|
cmds = seq_str.split("|")
|
||||||
|
fn_calls_list = [self.format_dict[cmd[0]].format(cmd[1:]) for cmd in cmds]
|
||||||
|
except Exception as e:
|
||||||
|
raise FatalError(f'Invalid "custom_reset_sequence" option format: {e}')
|
||||||
|
return "\n".join(fn_calls_list)
|
||||||
39
mixly/tools/python/esptool/targets/__init__.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
from .esp32 import ESP32ROM
|
||||||
|
from .esp32c2 import ESP32C2ROM
|
||||||
|
from .esp32c3 import ESP32C3ROM
|
||||||
|
from .esp32c5 import ESP32C5ROM
|
||||||
|
from .esp32c5beta3 import ESP32C5BETA3ROM
|
||||||
|
from .esp32c6 import ESP32C6ROM
|
||||||
|
from .esp32c61 import ESP32C61ROM
|
||||||
|
from .esp32c6beta import ESP32C6BETAROM
|
||||||
|
from .esp32h2 import ESP32H2ROM
|
||||||
|
from .esp32h2beta1 import ESP32H2BETA1ROM
|
||||||
|
from .esp32h2beta2 import ESP32H2BETA2ROM
|
||||||
|
from .esp32p4 import ESP32P4ROM
|
||||||
|
from .esp32s2 import ESP32S2ROM
|
||||||
|
from .esp32s3 import ESP32S3ROM
|
||||||
|
from .esp32s3beta2 import ESP32S3BETA2ROM
|
||||||
|
from .esp8266 import ESP8266ROM
|
||||||
|
|
||||||
|
|
||||||
|
CHIP_DEFS = {
|
||||||
|
"esp8266": ESP8266ROM,
|
||||||
|
"esp32": ESP32ROM,
|
||||||
|
"esp32s2": ESP32S2ROM,
|
||||||
|
"esp32s3beta2": ESP32S3BETA2ROM,
|
||||||
|
"esp32s3": ESP32S3ROM,
|
||||||
|
"esp32c3": ESP32C3ROM,
|
||||||
|
"esp32c6beta": ESP32C6BETAROM,
|
||||||
|
"esp32h2beta1": ESP32H2BETA1ROM,
|
||||||
|
"esp32h2beta2": ESP32H2BETA2ROM,
|
||||||
|
"esp32c2": ESP32C2ROM,
|
||||||
|
"esp32c6": ESP32C6ROM,
|
||||||
|
"esp32c61": ESP32C61ROM,
|
||||||
|
"esp32c5": ESP32C5ROM,
|
||||||
|
"esp32c5beta3": ESP32C5BETA3ROM,
|
||||||
|
"esp32h2": ESP32H2ROM,
|
||||||
|
"esp32p4": ESP32P4ROM,
|
||||||
|
}
|
||||||
|
|
||||||
|
CHIP_LIST = list(CHIP_DEFS.keys())
|
||||||
|
ROM_LIST = list(CHIP_DEFS.values())
|
||||||
473
mixly/tools/python/esptool/targets/esp32.py
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
|
||||||
|
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from ..loader import ESPLoader
|
||||||
|
from ..util import FatalError, NotSupportedError
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32ROM(ESPLoader):
|
||||||
|
"""Access class for ESP32 ROM bootloader"""
|
||||||
|
|
||||||
|
CHIP_NAME = "ESP32"
|
||||||
|
IMAGE_CHIP_ID = 0
|
||||||
|
IS_STUB = False
|
||||||
|
|
||||||
|
CHIP_DETECT_MAGIC_VALUE = [0x00F01D83]
|
||||||
|
|
||||||
|
IROM_MAP_START = 0x400D0000
|
||||||
|
IROM_MAP_END = 0x40400000
|
||||||
|
|
||||||
|
DROM_MAP_START = 0x3F400000
|
||||||
|
DROM_MAP_END = 0x3F800000
|
||||||
|
|
||||||
|
# ESP32 uses a 4 byte status reply
|
||||||
|
STATUS_BYTES_LENGTH = 4
|
||||||
|
|
||||||
|
SPI_REG_BASE = 0x3FF42000
|
||||||
|
SPI_USR_OFFS = 0x1C
|
||||||
|
SPI_USR1_OFFS = 0x20
|
||||||
|
SPI_USR2_OFFS = 0x24
|
||||||
|
SPI_MOSI_DLEN_OFFS = 0x28
|
||||||
|
SPI_MISO_DLEN_OFFS = 0x2C
|
||||||
|
EFUSE_RD_REG_BASE = 0x3FF5A000
|
||||||
|
|
||||||
|
EFUSE_BLK0_RDATA3_REG_OFFS = EFUSE_RD_REG_BASE + 0x00C
|
||||||
|
EFUSE_BLK0_RDATA5_REG_OFFS = EFUSE_RD_REG_BASE + 0x014
|
||||||
|
|
||||||
|
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + 0x18
|
||||||
|
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 7 # EFUSE_RD_DISABLE_DL_ENCRYPT
|
||||||
|
|
||||||
|
EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_RD_REG_BASE # EFUSE_BLK0_WDATA0_REG
|
||||||
|
EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7F << 20 # EFUSE_FLASH_CRYPT_CNT
|
||||||
|
|
||||||
|
EFUSE_RD_ABS_DONE_REG = EFUSE_RD_REG_BASE + 0x018
|
||||||
|
EFUSE_RD_ABS_DONE_0_MASK = 1 << 4
|
||||||
|
EFUSE_RD_ABS_DONE_1_MASK = 1 << 5
|
||||||
|
|
||||||
|
EFUSE_VDD_SPI_REG = EFUSE_RD_REG_BASE + 0x10
|
||||||
|
VDD_SPI_XPD = 1 << 14 # XPD_SDIO_REG
|
||||||
|
VDD_SPI_TIEH = 1 << 15 # XPD_SDIO_TIEH
|
||||||
|
VDD_SPI_FORCE = 1 << 16 # XPD_SDIO_FORCE
|
||||||
|
|
||||||
|
DR_REG_SYSCON_BASE = 0x3FF66000
|
||||||
|
APB_CTL_DATE_ADDR = DR_REG_SYSCON_BASE + 0x7C
|
||||||
|
APB_CTL_DATE_V = 0x1
|
||||||
|
APB_CTL_DATE_S = 31
|
||||||
|
|
||||||
|
SPI_W0_OFFS = 0x80
|
||||||
|
|
||||||
|
UART_CLKDIV_REG = 0x3FF40014
|
||||||
|
|
||||||
|
XTAL_CLK_DIVIDER = 1
|
||||||
|
|
||||||
|
RTCCALICFG1 = 0x3FF5F06C
|
||||||
|
TIMERS_RTC_CALI_VALUE = 0x01FFFFFF
|
||||||
|
TIMERS_RTC_CALI_VALUE_S = 7
|
||||||
|
|
||||||
|
GPIO_STRAP_REG = 0x3FF44038
|
||||||
|
GPIO_STRAP_VDDSPI_MASK = 1 << 5 # GPIO_STRAP_VDDSDIO
|
||||||
|
|
||||||
|
RTC_CNTL_SDIO_CONF_REG = 0x3FF48074
|
||||||
|
RTC_CNTL_XPD_SDIO_REG = 1 << 31
|
||||||
|
RTC_CNTL_DREFH_SDIO_M = 3 << 29
|
||||||
|
RTC_CNTL_DREFM_SDIO_M = 3 << 27
|
||||||
|
RTC_CNTL_DREFL_SDIO_M = 3 << 25
|
||||||
|
RTC_CNTL_SDIO_FORCE = 1 << 22
|
||||||
|
RTC_CNTL_SDIO_PD_EN = 1 << 21
|
||||||
|
|
||||||
|
FLASH_SIZES = {
|
||||||
|
"1MB": 0x00,
|
||||||
|
"2MB": 0x10,
|
||||||
|
"4MB": 0x20,
|
||||||
|
"8MB": 0x30,
|
||||||
|
"16MB": 0x40,
|
||||||
|
"32MB": 0x50,
|
||||||
|
"64MB": 0x60,
|
||||||
|
"128MB": 0x70,
|
||||||
|
}
|
||||||
|
|
||||||
|
FLASH_FREQUENCY = {
|
||||||
|
"80m": 0xF,
|
||||||
|
"40m": 0x0,
|
||||||
|
"26m": 0x1,
|
||||||
|
"20m": 0x2,
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOTLOADER_FLASH_OFFSET = 0x1000
|
||||||
|
|
||||||
|
OVERRIDE_VDDSDIO_CHOICES = ["1.8V", "1.9V", "OFF"]
|
||||||
|
|
||||||
|
MEMORY_MAP = [
|
||||||
|
[0x00000000, 0x00010000, "PADDING"],
|
||||||
|
[0x3F400000, 0x3F800000, "DROM"],
|
||||||
|
[0x3F800000, 0x3FC00000, "EXTRAM_DATA"],
|
||||||
|
[0x3FF80000, 0x3FF82000, "RTC_DRAM"],
|
||||||
|
[0x3FF90000, 0x40000000, "BYTE_ACCESSIBLE"],
|
||||||
|
[0x3FFAE000, 0x40000000, "DRAM"],
|
||||||
|
[0x3FFE0000, 0x3FFFFFFC, "DIRAM_DRAM"],
|
||||||
|
[0x40000000, 0x40070000, "IROM"],
|
||||||
|
[0x40070000, 0x40078000, "CACHE_PRO"],
|
||||||
|
[0x40078000, 0x40080000, "CACHE_APP"],
|
||||||
|
[0x40080000, 0x400A0000, "IRAM"],
|
||||||
|
[0x400A0000, 0x400BFFFC, "DIRAM_IRAM"],
|
||||||
|
[0x400C0000, 0x400C2000, "RTC_IRAM"],
|
||||||
|
[0x400D0000, 0x40400000, "IROM"],
|
||||||
|
[0x50000000, 0x50002000, "RTC_DATA"],
|
||||||
|
]
|
||||||
|
|
||||||
|
FLASH_ENCRYPTED_WRITE_ALIGN = 32
|
||||||
|
|
||||||
|
UF2_FAMILY_ID = 0x1C5F21B0
|
||||||
|
|
||||||
|
KEY_PURPOSES: Dict[int, str] = {}
|
||||||
|
|
||||||
|
""" Try to read the BLOCK1 (encryption key) and check if it is valid """
|
||||||
|
|
||||||
|
def is_flash_encryption_key_valid(self):
|
||||||
|
"""Bit 0 of efuse_rd_disable[3:0] is mapped to BLOCK1
|
||||||
|
this bit is at position 16 in EFUSE_BLK0_RDATA0_REG"""
|
||||||
|
word0 = self.read_efuse(0)
|
||||||
|
rd_disable = (word0 >> 16) & 0x1
|
||||||
|
|
||||||
|
# reading of BLOCK1 is NOT ALLOWED so we assume valid key is programmed
|
||||||
|
if rd_disable:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# reading of BLOCK1 is ALLOWED so we will read and verify for non-zero.
|
||||||
|
# When ESP32 has not generated AES/encryption key in BLOCK1,
|
||||||
|
# the contents will be readable and 0.
|
||||||
|
# If the flash encryption is enabled it is expected to have a valid
|
||||||
|
# non-zero key. We break out on first occurrence of non-zero value
|
||||||
|
key_word = [0] * 7
|
||||||
|
for i in range(len(key_word)):
|
||||||
|
key_word[i] = self.read_efuse(14 + i)
|
||||||
|
# key is non-zero so break & return
|
||||||
|
if key_word[i] != 0:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_flash_crypt_config(self):
|
||||||
|
"""For flash encryption related commands we need to make sure
|
||||||
|
user has programmed all the relevant efuse correctly so before
|
||||||
|
writing encrypted write_flash_encrypt esptool will verify the values
|
||||||
|
of flash_crypt_config to be non zero if they are not read
|
||||||
|
protected. If the values are zero a warning will be printed
|
||||||
|
|
||||||
|
bit 3 in efuse_rd_disable[3:0] is mapped to flash_crypt_config
|
||||||
|
this bit is at position 19 in EFUSE_BLK0_RDATA0_REG"""
|
||||||
|
word0 = self.read_efuse(0)
|
||||||
|
rd_disable = (word0 >> 19) & 0x1
|
||||||
|
|
||||||
|
if rd_disable == 0:
|
||||||
|
"""we can read the flash_crypt_config efuse value
|
||||||
|
so go & read it (EFUSE_BLK0_RDATA5_REG[31:28])"""
|
||||||
|
word5 = self.read_efuse(5)
|
||||||
|
word5 = (word5 >> 28) & 0xF
|
||||||
|
return word5
|
||||||
|
else:
|
||||||
|
# if read of the efuse is disabled we assume it is set correctly
|
||||||
|
return 0xF
|
||||||
|
|
||||||
|
def get_encrypted_download_disabled(self):
|
||||||
|
return (
|
||||||
|
self.read_reg(self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG)
|
||||||
|
& self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_flash_encryption_enabled(self):
|
||||||
|
flash_crypt_cnt = (
|
||||||
|
self.read_reg(self.EFUSE_SPI_BOOT_CRYPT_CNT_REG)
|
||||||
|
& self.EFUSE_SPI_BOOT_CRYPT_CNT_MASK
|
||||||
|
)
|
||||||
|
# Flash encryption enabled when odd number of bits are set
|
||||||
|
return bin(flash_crypt_cnt).count("1") & 1 != 0
|
||||||
|
|
||||||
|
def get_secure_boot_enabled(self):
|
||||||
|
efuses = self.read_reg(self.EFUSE_RD_ABS_DONE_REG)
|
||||||
|
rev = self.get_chip_revision()
|
||||||
|
return efuses & self.EFUSE_RD_ABS_DONE_0_MASK or (
|
||||||
|
rev >= 300 and efuses & self.EFUSE_RD_ABS_DONE_1_MASK
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_pkg_version(self):
|
||||||
|
word3 = self.read_efuse(3)
|
||||||
|
pkg_version = (word3 >> 9) & 0x07
|
||||||
|
pkg_version += ((word3 >> 2) & 0x1) << 3
|
||||||
|
return pkg_version
|
||||||
|
|
||||||
|
def get_chip_revision(self):
|
||||||
|
return self.get_major_chip_version() * 100 + self.get_minor_chip_version()
|
||||||
|
|
||||||
|
def get_minor_chip_version(self):
|
||||||
|
return (self.read_efuse(5) >> 24) & 0x3
|
||||||
|
|
||||||
|
def get_major_chip_version(self):
|
||||||
|
rev_bit0 = (self.read_efuse(3) >> 15) & 0x1
|
||||||
|
rev_bit1 = (self.read_efuse(5) >> 20) & 0x1
|
||||||
|
apb_ctl_date = self.read_reg(self.APB_CTL_DATE_ADDR)
|
||||||
|
rev_bit2 = (apb_ctl_date >> self.APB_CTL_DATE_S) & self.APB_CTL_DATE_V
|
||||||
|
combine_value = (rev_bit2 << 2) | (rev_bit1 << 1) | rev_bit0
|
||||||
|
|
||||||
|
revision = {
|
||||||
|
0: 0,
|
||||||
|
1: 1,
|
||||||
|
3: 2,
|
||||||
|
7: 3,
|
||||||
|
}.get(combine_value, 0)
|
||||||
|
return revision
|
||||||
|
|
||||||
|
def get_chip_description(self):
|
||||||
|
pkg_version = self.get_pkg_version()
|
||||||
|
major_rev = self.get_major_chip_version()
|
||||||
|
minor_rev = self.get_minor_chip_version()
|
||||||
|
rev3 = major_rev == 3
|
||||||
|
sc = self.read_efuse(3) & (1 << 0) # single core, CHIP_VER DIS_APP_CPU
|
||||||
|
|
||||||
|
chip_name = {
|
||||||
|
0: "ESP32-S0WDQ6" if sc else "ESP32-D0WDQ6-V3" if rev3 else "ESP32-D0WDQ6",
|
||||||
|
1: "ESP32-S0WD" if sc else "ESP32-D0WD-V3" if rev3 else "ESP32-D0WD",
|
||||||
|
2: "ESP32-D2WD",
|
||||||
|
4: "ESP32-U4WDH",
|
||||||
|
5: "ESP32-PICO-V3" if rev3 else "ESP32-PICO-D4",
|
||||||
|
6: "ESP32-PICO-V3-02",
|
||||||
|
7: "ESP32-D0WDR2-V3",
|
||||||
|
}.get(pkg_version, "unknown ESP32")
|
||||||
|
|
||||||
|
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
|
||||||
|
|
||||||
|
def get_chip_features(self):
|
||||||
|
features = ["WiFi"]
|
||||||
|
word3 = self.read_efuse(3)
|
||||||
|
|
||||||
|
# names of variables in this section are lowercase
|
||||||
|
# versions of EFUSE names as documented in TRM and
|
||||||
|
# ESP-IDF efuse_reg.h
|
||||||
|
|
||||||
|
chip_ver_dis_bt = word3 & (1 << 1)
|
||||||
|
if chip_ver_dis_bt == 0:
|
||||||
|
features += ["BT"]
|
||||||
|
|
||||||
|
chip_ver_dis_app_cpu = word3 & (1 << 0)
|
||||||
|
if chip_ver_dis_app_cpu:
|
||||||
|
features += ["Single Core"]
|
||||||
|
else:
|
||||||
|
features += ["Dual Core"]
|
||||||
|
|
||||||
|
chip_cpu_freq_rated = word3 & (1 << 13)
|
||||||
|
if chip_cpu_freq_rated:
|
||||||
|
chip_cpu_freq_low = word3 & (1 << 12)
|
||||||
|
if chip_cpu_freq_low:
|
||||||
|
features += ["160MHz"]
|
||||||
|
else:
|
||||||
|
features += ["240MHz"]
|
||||||
|
|
||||||
|
pkg_version = self.get_pkg_version()
|
||||||
|
if pkg_version in [2, 4, 5, 6]:
|
||||||
|
features += ["Embedded Flash"]
|
||||||
|
|
||||||
|
if pkg_version == 6:
|
||||||
|
features += ["Embedded PSRAM"]
|
||||||
|
|
||||||
|
word4 = self.read_efuse(4)
|
||||||
|
adc_vref = (word4 >> 8) & 0x1F
|
||||||
|
if adc_vref:
|
||||||
|
features += ["VRef calibration in efuse"]
|
||||||
|
|
||||||
|
blk3_part_res = word3 >> 14 & 0x1
|
||||||
|
if blk3_part_res:
|
||||||
|
features += ["BLK3 partially reserved"]
|
||||||
|
|
||||||
|
word6 = self.read_efuse(6)
|
||||||
|
coding_scheme = word6 & 0x3
|
||||||
|
features += [
|
||||||
|
"Coding Scheme %s"
|
||||||
|
% {
|
||||||
|
0: "None",
|
||||||
|
1: "3/4",
|
||||||
|
2: "Repeat (UNSUPPORTED)",
|
||||||
|
3: "None (may contain encoding data)",
|
||||||
|
}[coding_scheme]
|
||||||
|
]
|
||||||
|
|
||||||
|
return features
|
||||||
|
|
||||||
|
def get_chip_spi_pads(self):
|
||||||
|
"""Read chip spi pad config
|
||||||
|
return: clk, q, d, hd, cd
|
||||||
|
"""
|
||||||
|
efuse_blk0_rdata5 = self.read_reg(self.EFUSE_BLK0_RDATA5_REG_OFFS)
|
||||||
|
spi_pad_clk = efuse_blk0_rdata5 & 0x1F
|
||||||
|
spi_pad_q = (efuse_blk0_rdata5 >> 5) & 0x1F
|
||||||
|
spi_pad_d = (efuse_blk0_rdata5 >> 10) & 0x1F
|
||||||
|
spi_pad_cs = (efuse_blk0_rdata5 >> 15) & 0x1F
|
||||||
|
|
||||||
|
efuse_blk0_rdata3_reg = self.read_reg(self.EFUSE_BLK0_RDATA3_REG_OFFS)
|
||||||
|
spi_pad_hd = (efuse_blk0_rdata3_reg >> 4) & 0x1F
|
||||||
|
return spi_pad_clk, spi_pad_q, spi_pad_d, spi_pad_hd, spi_pad_cs
|
||||||
|
|
||||||
|
def read_efuse(self, n):
|
||||||
|
"""Read the nth word of the ESP3x EFUSE region."""
|
||||||
|
return self.read_reg(self.EFUSE_RD_REG_BASE + (4 * n))
|
||||||
|
|
||||||
|
def chip_id(self):
|
||||||
|
raise NotSupportedError(self, "Function chip_id")
|
||||||
|
|
||||||
|
def read_mac(self, mac_type="BASE_MAC"):
|
||||||
|
"""Read MAC from EFUSE region"""
|
||||||
|
if mac_type != "BASE_MAC":
|
||||||
|
return None
|
||||||
|
words = [self.read_efuse(2), self.read_efuse(1)]
|
||||||
|
bitstring = struct.pack(">II", *words)
|
||||||
|
bitstring = bitstring[2:8] # trim the 2 byte CRC
|
||||||
|
return tuple(bitstring)
|
||||||
|
|
||||||
|
def get_erase_size(self, offset, size):
|
||||||
|
return size
|
||||||
|
|
||||||
|
def _get_efuse_flash_voltage(self) -> Optional[str]:
|
||||||
|
efuse = self.read_reg(self.EFUSE_VDD_SPI_REG)
|
||||||
|
# check efuse setting
|
||||||
|
if efuse & (self.VDD_SPI_FORCE | self.VDD_SPI_XPD | self.VDD_SPI_TIEH):
|
||||||
|
return "3.3V"
|
||||||
|
elif efuse & (self.VDD_SPI_FORCE | self.VDD_SPI_XPD):
|
||||||
|
return "1.8V"
|
||||||
|
elif efuse & self.VDD_SPI_FORCE:
|
||||||
|
return "OFF"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_rtc_cntl_flash_voltage(self) -> Optional[str]:
|
||||||
|
reg = self.read_reg(self.RTC_CNTL_SDIO_CONF_REG)
|
||||||
|
# check if override is set in RTC_CNTL_SDIO_CONF_REG
|
||||||
|
if reg & self.RTC_CNTL_SDIO_FORCE:
|
||||||
|
if reg & self.RTC_CNTL_DREFH_SDIO_M:
|
||||||
|
return "1.9V"
|
||||||
|
elif reg & self.RTC_CNTL_XPD_SDIO_REG:
|
||||||
|
return "1.8V"
|
||||||
|
else:
|
||||||
|
return "OFF"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_flash_voltage(self):
|
||||||
|
"""Get flash voltage setting and print it to the console."""
|
||||||
|
voltage = self._get_rtc_cntl_flash_voltage()
|
||||||
|
source = "RTC_CNTL"
|
||||||
|
if not voltage:
|
||||||
|
voltage = self._get_efuse_flash_voltage()
|
||||||
|
source = "eFuse"
|
||||||
|
if not voltage:
|
||||||
|
strap_reg = self.read_reg(self.GPIO_STRAP_REG)
|
||||||
|
strap_reg &= self.GPIO_STRAP_VDDSPI_MASK
|
||||||
|
voltage = "1.8V" if strap_reg else "3.3V"
|
||||||
|
source = "a strapping pin"
|
||||||
|
print(f"Flash voltage set by {source} to {voltage}")
|
||||||
|
|
||||||
|
def override_vddsdio(self, new_voltage):
|
||||||
|
new_voltage = new_voltage.upper()
|
||||||
|
if new_voltage not in self.OVERRIDE_VDDSDIO_CHOICES:
|
||||||
|
raise FatalError(
|
||||||
|
f"The only accepted VDDSDIO overrides are {', '.join(self.OVERRIDE_VDDSDIO_CHOICES)}"
|
||||||
|
)
|
||||||
|
# RTC_CNTL_SDIO_TIEH is not used here, setting TIEH=1 would set 3.3V output,
|
||||||
|
# not safe for esptool.py to do
|
||||||
|
|
||||||
|
reg_val = self.RTC_CNTL_SDIO_FORCE # override efuse setting
|
||||||
|
reg_val |= self.RTC_CNTL_SDIO_PD_EN
|
||||||
|
if new_voltage != "OFF":
|
||||||
|
reg_val |= self.RTC_CNTL_XPD_SDIO_REG # enable internal LDO
|
||||||
|
if new_voltage == "1.9V":
|
||||||
|
reg_val |= (
|
||||||
|
self.RTC_CNTL_DREFH_SDIO_M
|
||||||
|
| self.RTC_CNTL_DREFM_SDIO_M
|
||||||
|
| self.RTC_CNTL_DREFL_SDIO_M
|
||||||
|
) # boost voltage
|
||||||
|
self.write_reg(self.RTC_CNTL_SDIO_CONF_REG, reg_val)
|
||||||
|
print("VDDSDIO regulator set to %s" % new_voltage)
|
||||||
|
|
||||||
|
def read_flash_slow(self, offset, length, progress_fn):
|
||||||
|
BLOCK_LEN = 64 # ROM read limit per command (this limit is why it's so slow)
|
||||||
|
|
||||||
|
data = b""
|
||||||
|
while len(data) < length:
|
||||||
|
block_len = min(BLOCK_LEN, length - len(data))
|
||||||
|
try:
|
||||||
|
r = self.check_command(
|
||||||
|
"read flash block",
|
||||||
|
self.ESP_READ_FLASH_SLOW,
|
||||||
|
struct.pack("<II", offset + len(data), block_len),
|
||||||
|
)
|
||||||
|
except FatalError:
|
||||||
|
print(
|
||||||
|
"Hint: Consider specifying flash size using '--flash_size' argument"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
if len(r) < block_len:
|
||||||
|
raise FatalError(
|
||||||
|
"Expected %d byte block, got %d bytes. Serial errors?"
|
||||||
|
% (block_len, len(r))
|
||||||
|
)
|
||||||
|
# command always returns 64 byte buffer,
|
||||||
|
# regardless of how many bytes were actually read from flash
|
||||||
|
data += r[:block_len]
|
||||||
|
if progress_fn and (len(data) % 1024 == 0 or len(data) == length):
|
||||||
|
progress_fn(len(data), length)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_rom_cal_crystal_freq(self):
|
||||||
|
"""
|
||||||
|
Get the crystal frequency calculated by the ROM
|
||||||
|
"""
|
||||||
|
# - Simulate the calculation in the ROM to get the XTAL frequency
|
||||||
|
# calculated by the ROM
|
||||||
|
|
||||||
|
cali_val = (
|
||||||
|
self.read_reg(self.RTCCALICFG1) >> self.TIMERS_RTC_CALI_VALUE_S
|
||||||
|
) & self.TIMERS_RTC_CALI_VALUE
|
||||||
|
clk_8M_freq = self.read_efuse(4) & (0xFF) # EFUSE_RD_CK8M_FREQ
|
||||||
|
rom_calculated_freq = cali_val * 15625 * clk_8M_freq / 40
|
||||||
|
return rom_calculated_freq
|
||||||
|
|
||||||
|
def change_baud(self, baud):
|
||||||
|
assert self.CHIP_NAME == "ESP32", "This workaround should only apply to ESP32"
|
||||||
|
# It's a workaround to avoid esp32 CK_8M frequency drift.
|
||||||
|
rom_calculated_freq = self.get_rom_cal_crystal_freq()
|
||||||
|
valid_freq = 40000000 if rom_calculated_freq > 33000000 else 26000000
|
||||||
|
false_rom_baud = int(baud * rom_calculated_freq // valid_freq)
|
||||||
|
|
||||||
|
print(f"Changing baud rate to {baud}")
|
||||||
|
self.command(self.ESP_CHANGE_BAUDRATE, struct.pack("<II", false_rom_baud, 0))
|
||||||
|
print("Changed.")
|
||||||
|
self._set_port_baudrate(baud)
|
||||||
|
time.sleep(0.05) # get rid of garbage sent during baud rate change
|
||||||
|
self.flush_input()
|
||||||
|
|
||||||
|
def check_spi_connection(self, spi_connection):
|
||||||
|
# Pins 30, 31 do not exist
|
||||||
|
if not set(spi_connection).issubset(set(range(0, 30)) | set((32, 33))):
|
||||||
|
raise FatalError("SPI Pin numbers must be in the range 0-29, 32, or 33.")
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32StubLoader(ESP32ROM):
|
||||||
|
"""Access class for ESP32 stub loader, runs on top of ROM."""
|
||||||
|
|
||||||
|
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
|
||||||
|
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
|
||||||
|
IS_STUB = True
|
||||||
|
|
||||||
|
def __init__(self, rom_loader):
|
||||||
|
self.secure_download_mode = rom_loader.secure_download_mode
|
||||||
|
self._port = rom_loader._port
|
||||||
|
self._trace_enabled = rom_loader._trace_enabled
|
||||||
|
self.cache = rom_loader.cache
|
||||||
|
self.flush_input() # resets _slip_reader
|
||||||
|
|
||||||
|
def change_baud(self, baud):
|
||||||
|
ESPLoader.change_baud(self, baud)
|
||||||
|
|
||||||
|
|
||||||
|
ESP32ROM.STUB_CLASS = ESP32StubLoader
|
||||||
185
mixly/tools/python/esptool/targets/esp32c2.py
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
|
||||||
|
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from .esp32c3 import ESP32C3ROM
|
||||||
|
from ..loader import ESPLoader
|
||||||
|
from ..util import FatalError
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32C2ROM(ESP32C3ROM):
|
||||||
|
CHIP_NAME = "ESP32-C2"
|
||||||
|
IMAGE_CHIP_ID = 12
|
||||||
|
|
||||||
|
IROM_MAP_START = 0x42000000
|
||||||
|
IROM_MAP_END = 0x42400000
|
||||||
|
DROM_MAP_START = 0x3C000000
|
||||||
|
DROM_MAP_END = 0x3C400000
|
||||||
|
|
||||||
|
# Magic value for ESP32C2 ECO0 , ECO1 and ECO4 respectively
|
||||||
|
CHIP_DETECT_MAGIC_VALUE = [0x6F51306F, 0x7C41A06F, 0x0C21E06F]
|
||||||
|
|
||||||
|
EFUSE_BASE = 0x60008800
|
||||||
|
EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x040
|
||||||
|
MAC_EFUSE_REG = EFUSE_BASE + 0x040
|
||||||
|
|
||||||
|
EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x30
|
||||||
|
EFUSE_SECURE_BOOT_EN_MASK = 1 << 21
|
||||||
|
|
||||||
|
EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x30
|
||||||
|
EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
|
||||||
|
|
||||||
|
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_BASE + 0x30
|
||||||
|
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 6
|
||||||
|
|
||||||
|
EFUSE_XTS_KEY_LENGTH_256_REG = EFUSE_BASE + 0x30
|
||||||
|
EFUSE_XTS_KEY_LENGTH_256 = 1 << 10
|
||||||
|
|
||||||
|
EFUSE_BLOCK_KEY0_REG = EFUSE_BASE + 0x60
|
||||||
|
|
||||||
|
EFUSE_RD_DIS_REG = EFUSE_BASE + 0x30
|
||||||
|
EFUSE_RD_DIS = 3
|
||||||
|
|
||||||
|
FLASH_FREQUENCY = {
|
||||||
|
"60m": 0xF,
|
||||||
|
"30m": 0x0,
|
||||||
|
"20m": 0x1,
|
||||||
|
"15m": 0x2,
|
||||||
|
}
|
||||||
|
|
||||||
|
MEMORY_MAP = [
|
||||||
|
[0x00000000, 0x00010000, "PADDING"],
|
||||||
|
[0x3C000000, 0x3C400000, "DROM"],
|
||||||
|
[0x3FCA0000, 0x3FCE0000, "DRAM"],
|
||||||
|
[0x3FC88000, 0x3FD00000, "BYTE_ACCESSIBLE"],
|
||||||
|
[0x3FF00000, 0x3FF50000, "DROM_MASK"],
|
||||||
|
[0x40000000, 0x40090000, "IROM_MASK"],
|
||||||
|
[0x42000000, 0x42400000, "IROM"],
|
||||||
|
[0x4037C000, 0x403C0000, "IRAM"],
|
||||||
|
]
|
||||||
|
|
||||||
|
UF2_FAMILY_ID = 0x2B88D29C
|
||||||
|
|
||||||
|
KEY_PURPOSES: Dict[int, str] = {}
|
||||||
|
|
||||||
|
def get_pkg_version(self):
|
||||||
|
num_word = 1
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 22) & 0x07
|
||||||
|
|
||||||
|
def get_chip_description(self):
|
||||||
|
chip_name = {
|
||||||
|
0: "ESP32-C2",
|
||||||
|
1: "ESP32-C2",
|
||||||
|
}.get(self.get_pkg_version(), "unknown ESP32-C2")
|
||||||
|
major_rev = self.get_major_chip_version()
|
||||||
|
minor_rev = self.get_minor_chip_version()
|
||||||
|
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
|
||||||
|
|
||||||
|
def get_minor_chip_version(self):
|
||||||
|
num_word = 1
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 16) & 0xF
|
||||||
|
|
||||||
|
def get_major_chip_version(self):
|
||||||
|
num_word = 1
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 20) & 0x3
|
||||||
|
|
||||||
|
def get_flash_cap(self):
|
||||||
|
# ESP32-C2 doesn't have eFuse field FLASH_CAP.
|
||||||
|
# Can't get info about the flash chip.
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_flash_vendor(self):
|
||||||
|
# ESP32-C2 doesn't have eFuse field FLASH_VENDOR.
|
||||||
|
# Can't get info about the flash chip.
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_crystal_freq(self):
|
||||||
|
# The crystal detection algorithm of ESP32/ESP8266 works for ESP32-C2 as well.
|
||||||
|
return ESPLoader.get_crystal_freq(self)
|
||||||
|
|
||||||
|
def change_baud(self, baud):
|
||||||
|
rom_with_26M_XTAL = not self.IS_STUB and self.get_crystal_freq() == 26
|
||||||
|
if rom_with_26M_XTAL:
|
||||||
|
# The code is copied over from ESPLoader.change_baud().
|
||||||
|
# Probably this is just a temporary solution until the next chip revision.
|
||||||
|
|
||||||
|
# The ROM code thinks it uses a 40 MHz XTAL. Recompute the baud rate
|
||||||
|
# in order to trick the ROM code to set the correct baud rate for
|
||||||
|
# a 26 MHz XTAL.
|
||||||
|
false_rom_baud = baud * 40 // 26
|
||||||
|
|
||||||
|
print(f"Changing baud rate to {baud}")
|
||||||
|
self.command(
|
||||||
|
self.ESP_CHANGE_BAUDRATE, struct.pack("<II", false_rom_baud, 0)
|
||||||
|
)
|
||||||
|
print("Changed.")
|
||||||
|
self._set_port_baudrate(baud)
|
||||||
|
time.sleep(0.05) # get rid of garbage sent during baud rate change
|
||||||
|
self.flush_input()
|
||||||
|
else:
|
||||||
|
ESPLoader.change_baud(self, baud)
|
||||||
|
|
||||||
|
def _post_connect(self):
|
||||||
|
# ESP32C2 ECO0 is no longer supported by the flasher stub
|
||||||
|
if not self.secure_download_mode and self.get_chip_revision() == 0:
|
||||||
|
self.stub_is_disabled = True
|
||||||
|
self.IS_STUB = False
|
||||||
|
|
||||||
|
""" Try to read (encryption key) and check if it is valid """
|
||||||
|
|
||||||
|
def is_flash_encryption_key_valid(self):
|
||||||
|
key_len_256 = (
|
||||||
|
self.read_reg(self.EFUSE_XTS_KEY_LENGTH_256_REG)
|
||||||
|
& self.EFUSE_XTS_KEY_LENGTH_256
|
||||||
|
)
|
||||||
|
|
||||||
|
word0 = self.read_reg(self.EFUSE_RD_DIS_REG) & self.EFUSE_RD_DIS
|
||||||
|
rd_disable = word0 == 3 if key_len_256 else word0 == 1
|
||||||
|
|
||||||
|
# reading of BLOCK3 is NOT ALLOWED so we assume valid key is programmed
|
||||||
|
if rd_disable:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# reading of BLOCK3 is ALLOWED so we will read and verify for non-zero.
|
||||||
|
# When chip has not generated AES/encryption key in BLOCK3,
|
||||||
|
# the contents will be readable and 0.
|
||||||
|
# If the flash encryption is enabled it is expected to have a valid
|
||||||
|
# non-zero key. We break out on first occurrence of non-zero value
|
||||||
|
key_word = [0] * 7 if key_len_256 else [0] * 3
|
||||||
|
for i in range(len(key_word)):
|
||||||
|
key_word[i] = self.read_reg(self.EFUSE_BLOCK_KEY0_REG + i * 4)
|
||||||
|
# key is non-zero so break & return
|
||||||
|
if key_word[i] != 0:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_spi_connection(self, spi_connection):
|
||||||
|
if not set(spi_connection).issubset(set(range(0, 21))):
|
||||||
|
raise FatalError("SPI Pin numbers must be in the range 0-20.")
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32C2StubLoader(ESP32C2ROM):
|
||||||
|
"""Access class for ESP32C2 stub loader, runs on top of ROM.
|
||||||
|
|
||||||
|
(Basically the same as ESP32StubLoader, but different base class.
|
||||||
|
Can possibly be made into a mixin.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
|
||||||
|
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
|
||||||
|
IS_STUB = True
|
||||||
|
|
||||||
|
def __init__(self, rom_loader):
|
||||||
|
self.secure_download_mode = rom_loader.secure_download_mode
|
||||||
|
self._port = rom_loader._port
|
||||||
|
self._trace_enabled = rom_loader._trace_enabled
|
||||||
|
self.cache = rom_loader.cache
|
||||||
|
self.flush_input() # resets _slip_reader
|
||||||
|
|
||||||
|
|
||||||
|
ESP32C2ROM.STUB_CLASS = ESP32C2StubLoader
|
||||||
284
mixly/tools/python/esptool/targets/esp32c3.py
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
|
||||||
|
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import struct
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from .esp32 import ESP32ROM
|
||||||
|
from ..loader import ESPLoader
|
||||||
|
from ..util import FatalError, NotImplementedInROMError
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32C3ROM(ESP32ROM):
|
||||||
|
CHIP_NAME = "ESP32-C3"
|
||||||
|
IMAGE_CHIP_ID = 5
|
||||||
|
|
||||||
|
IROM_MAP_START = 0x42000000
|
||||||
|
IROM_MAP_END = 0x42800000
|
||||||
|
DROM_MAP_START = 0x3C000000
|
||||||
|
DROM_MAP_END = 0x3C800000
|
||||||
|
|
||||||
|
SPI_REG_BASE = 0x60002000
|
||||||
|
SPI_USR_OFFS = 0x18
|
||||||
|
SPI_USR1_OFFS = 0x1C
|
||||||
|
SPI_USR2_OFFS = 0x20
|
||||||
|
SPI_MOSI_DLEN_OFFS = 0x24
|
||||||
|
SPI_MISO_DLEN_OFFS = 0x28
|
||||||
|
SPI_W0_OFFS = 0x58
|
||||||
|
|
||||||
|
SPI_ADDR_REG_MSB = False
|
||||||
|
|
||||||
|
BOOTLOADER_FLASH_OFFSET = 0x0
|
||||||
|
|
||||||
|
# Magic values for ESP32-C3 eco 1+2, eco 3, eco 6, and eco 7 respectively
|
||||||
|
CHIP_DETECT_MAGIC_VALUE = [0x6921506F, 0x1B31506F, 0x4881606F, 0x4361606F]
|
||||||
|
|
||||||
|
UART_DATE_REG_ADDR = 0x60000000 + 0x7C
|
||||||
|
|
||||||
|
UART_CLKDIV_REG = 0x60000014
|
||||||
|
|
||||||
|
EFUSE_BASE = 0x60008800
|
||||||
|
EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
|
||||||
|
MAC_EFUSE_REG = EFUSE_BASE + 0x044
|
||||||
|
|
||||||
|
EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
|
||||||
|
|
||||||
|
EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
|
||||||
|
EFUSE_PURPOSE_KEY0_SHIFT = 24
|
||||||
|
EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
|
||||||
|
EFUSE_PURPOSE_KEY1_SHIFT = 28
|
||||||
|
EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY2_SHIFT = 0
|
||||||
|
EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY3_SHIFT = 4
|
||||||
|
EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY4_SHIFT = 8
|
||||||
|
EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY5_SHIFT = 12
|
||||||
|
|
||||||
|
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
|
||||||
|
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
|
||||||
|
|
||||||
|
EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
|
||||||
|
EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
|
||||||
|
|
||||||
|
EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
|
||||||
|
EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
|
||||||
|
|
||||||
|
PURPOSE_VAL_XTS_AES128_KEY = 4
|
||||||
|
|
||||||
|
SUPPORTS_ENCRYPTED_FLASH = True
|
||||||
|
|
||||||
|
FLASH_ENCRYPTED_WRITE_ALIGN = 16
|
||||||
|
|
||||||
|
UARTDEV_BUF_NO = 0x3FCDF07C # Variable in ROM .bss which indicates the port in use
|
||||||
|
UARTDEV_BUF_NO_USB_JTAG_SERIAL = 3 # The above var when USB-JTAG/Serial is used
|
||||||
|
|
||||||
|
RTCCNTL_BASE_REG = 0x60008000
|
||||||
|
RTC_CNTL_SWD_CONF_REG = RTCCNTL_BASE_REG + 0x00AC
|
||||||
|
RTC_CNTL_SWD_AUTO_FEED_EN = 1 << 31
|
||||||
|
RTC_CNTL_SWD_WPROTECT_REG = RTCCNTL_BASE_REG + 0x00B0
|
||||||
|
RTC_CNTL_SWD_WKEY = 0x8F1D312A
|
||||||
|
|
||||||
|
RTC_CNTL_WDTCONFIG0_REG = RTCCNTL_BASE_REG + 0x0090
|
||||||
|
RTC_CNTL_WDTWPROTECT_REG = RTCCNTL_BASE_REG + 0x00A8
|
||||||
|
RTC_CNTL_WDT_WKEY = 0x50D83AA1
|
||||||
|
|
||||||
|
MEMORY_MAP = [
|
||||||
|
[0x00000000, 0x00010000, "PADDING"],
|
||||||
|
[0x3C000000, 0x3C800000, "DROM"],
|
||||||
|
[0x3FC80000, 0x3FCE0000, "DRAM"],
|
||||||
|
[0x3FC88000, 0x3FD00000, "BYTE_ACCESSIBLE"],
|
||||||
|
[0x3FF00000, 0x3FF20000, "DROM_MASK"],
|
||||||
|
[0x40000000, 0x40060000, "IROM_MASK"],
|
||||||
|
[0x42000000, 0x42800000, "IROM"],
|
||||||
|
[0x4037C000, 0x403E0000, "IRAM"],
|
||||||
|
[0x50000000, 0x50002000, "RTC_IRAM"],
|
||||||
|
[0x50000000, 0x50002000, "RTC_DRAM"],
|
||||||
|
[0x600FE000, 0x60100000, "MEM_INTERNAL2"],
|
||||||
|
]
|
||||||
|
|
||||||
|
UF2_FAMILY_ID = 0xD42BA06C
|
||||||
|
|
||||||
|
EFUSE_MAX_KEY = 5
|
||||||
|
KEY_PURPOSES: Dict[int, str] = {
|
||||||
|
0: "USER/EMPTY",
|
||||||
|
1: "RESERVED",
|
||||||
|
4: "XTS_AES_128_KEY",
|
||||||
|
5: "HMAC_DOWN_ALL",
|
||||||
|
6: "HMAC_DOWN_JTAG",
|
||||||
|
7: "HMAC_DOWN_DIGITAL_SIGNATURE",
|
||||||
|
8: "HMAC_UP",
|
||||||
|
9: "SECURE_BOOT_DIGEST0",
|
||||||
|
10: "SECURE_BOOT_DIGEST1",
|
||||||
|
11: "SECURE_BOOT_DIGEST2",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_pkg_version(self):
|
||||||
|
num_word = 3
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x07
|
||||||
|
|
||||||
|
def get_minor_chip_version(self):
|
||||||
|
hi_num_word = 5
|
||||||
|
hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01
|
||||||
|
low_num_word = 3
|
||||||
|
low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07
|
||||||
|
return (hi << 3) + low
|
||||||
|
|
||||||
|
def get_major_chip_version(self):
|
||||||
|
num_word = 5
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03
|
||||||
|
|
||||||
|
def get_flash_cap(self):
|
||||||
|
num_word = 3
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 27) & 0x07
|
||||||
|
|
||||||
|
def get_flash_vendor(self):
|
||||||
|
num_word = 4
|
||||||
|
vendor_id = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x07
|
||||||
|
return {1: "XMC", 2: "GD", 3: "FM", 4: "TT", 5: "ZBIT"}.get(vendor_id, "")
|
||||||
|
|
||||||
|
def get_chip_description(self):
|
||||||
|
chip_name = {
|
||||||
|
0: "ESP32-C3 (QFN32)",
|
||||||
|
1: "ESP8685 (QFN28)",
|
||||||
|
2: "ESP32-C3 AZ (QFN32)",
|
||||||
|
3: "ESP8686 (QFN24)",
|
||||||
|
}.get(self.get_pkg_version(), "unknown ESP32-C3")
|
||||||
|
major_rev = self.get_major_chip_version()
|
||||||
|
minor_rev = self.get_minor_chip_version()
|
||||||
|
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
|
||||||
|
|
||||||
|
def get_chip_features(self):
|
||||||
|
features = ["WiFi", "BLE"]
|
||||||
|
|
||||||
|
flash = {
|
||||||
|
0: None,
|
||||||
|
1: "Embedded Flash 4MB",
|
||||||
|
2: "Embedded Flash 2MB",
|
||||||
|
3: "Embedded Flash 1MB",
|
||||||
|
4: "Embedded Flash 8MB",
|
||||||
|
}.get(self.get_flash_cap(), "Unknown Embedded Flash")
|
||||||
|
if flash is not None:
|
||||||
|
features += [flash + f" ({self.get_flash_vendor()})"]
|
||||||
|
return features
|
||||||
|
|
||||||
|
def get_crystal_freq(self):
|
||||||
|
# ESP32C3 XTAL is fixed to 40MHz
|
||||||
|
return 40
|
||||||
|
|
||||||
|
def get_flash_voltage(self):
|
||||||
|
pass # not supported on ESP32-C3
|
||||||
|
|
||||||
|
def override_vddsdio(self, new_voltage):
|
||||||
|
raise NotImplementedInROMError(
|
||||||
|
"VDD_SDIO overrides are not supported for ESP32-C3"
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_mac(self, mac_type="BASE_MAC"):
|
||||||
|
"""Read MAC from EFUSE region"""
|
||||||
|
if mac_type != "BASE_MAC":
|
||||||
|
return None
|
||||||
|
mac0 = self.read_reg(self.MAC_EFUSE_REG)
|
||||||
|
mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
|
||||||
|
bitstring = struct.pack(">II", mac1, mac0)[2:]
|
||||||
|
return tuple(bitstring)
|
||||||
|
|
||||||
|
def get_flash_crypt_config(self):
|
||||||
|
return None # doesn't exist on ESP32-C3
|
||||||
|
|
||||||
|
def get_secure_boot_enabled(self):
|
||||||
|
return (
|
||||||
|
self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
|
||||||
|
& self.EFUSE_SECURE_BOOT_EN_MASK
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_key_block_purpose(self, key_block):
|
||||||
|
if key_block < 0 or key_block > self.EFUSE_MAX_KEY:
|
||||||
|
raise FatalError(
|
||||||
|
f"Valid key block numbers must be in range 0-{self.EFUSE_MAX_KEY}"
|
||||||
|
)
|
||||||
|
|
||||||
|
reg, shift = [
|
||||||
|
(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
|
||||||
|
][key_block]
|
||||||
|
return (self.read_reg(reg) >> shift) & 0xF
|
||||||
|
|
||||||
|
def is_flash_encryption_key_valid(self):
|
||||||
|
# Need to see an AES-128 key
|
||||||
|
purposes = [
|
||||||
|
self.get_key_block_purpose(b) for b in range(self.EFUSE_MAX_KEY + 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes)
|
||||||
|
|
||||||
|
def change_baud(self, baud):
|
||||||
|
ESPLoader.change_baud(self, baud)
|
||||||
|
|
||||||
|
def uses_usb_jtag_serial(self):
|
||||||
|
"""
|
||||||
|
Check the UARTDEV_BUF_NO register to see if USB-JTAG/Serial is being used
|
||||||
|
"""
|
||||||
|
if self.secure_download_mode:
|
||||||
|
return False # Can't detect USB-JTAG/Serial in secure download mode
|
||||||
|
return self.get_uart_no() == self.UARTDEV_BUF_NO_USB_JTAG_SERIAL
|
||||||
|
|
||||||
|
def disable_watchdogs(self):
|
||||||
|
# When USB-JTAG/Serial is used, the RTC WDT and SWD watchdog are not reset
|
||||||
|
# and can then reset the board during flashing. Disable or autofeed them.
|
||||||
|
if self.uses_usb_jtag_serial():
|
||||||
|
# Disable RTC WDT
|
||||||
|
self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, self.RTC_CNTL_WDT_WKEY)
|
||||||
|
self.write_reg(self.RTC_CNTL_WDTCONFIG0_REG, 0)
|
||||||
|
self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, 0)
|
||||||
|
|
||||||
|
# Automatically feed SWD
|
||||||
|
self.write_reg(self.RTC_CNTL_SWD_WPROTECT_REG, self.RTC_CNTL_SWD_WKEY)
|
||||||
|
self.write_reg(
|
||||||
|
self.RTC_CNTL_SWD_CONF_REG,
|
||||||
|
self.read_reg(self.RTC_CNTL_SWD_CONF_REG)
|
||||||
|
| self.RTC_CNTL_SWD_AUTO_FEED_EN,
|
||||||
|
)
|
||||||
|
self.write_reg(self.RTC_CNTL_SWD_WPROTECT_REG, 0)
|
||||||
|
|
||||||
|
def _post_connect(self):
|
||||||
|
if not self.sync_stub_detected: # Don't run if stub is reused
|
||||||
|
self.disable_watchdogs()
|
||||||
|
|
||||||
|
def check_spi_connection(self, spi_connection):
|
||||||
|
if not set(spi_connection).issubset(set(range(0, 22))):
|
||||||
|
raise FatalError("SPI Pin numbers must be in the range 0-21.")
|
||||||
|
if any([v for v in spi_connection if v in [18, 19]]):
|
||||||
|
print(
|
||||||
|
"WARNING: GPIO pins 18 and 19 are used by USB-Serial/JTAG, "
|
||||||
|
"consider using other pins for SPI flash connection."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32C3StubLoader(ESP32C3ROM):
|
||||||
|
"""Access class for ESP32C3 stub loader, runs on top of ROM.
|
||||||
|
|
||||||
|
(Basically the same as ESP32StubLoader, but different base class.
|
||||||
|
Can possibly be made into a mixin.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
|
||||||
|
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
|
||||||
|
IS_STUB = True
|
||||||
|
|
||||||
|
def __init__(self, rom_loader):
|
||||||
|
self.secure_download_mode = rom_loader.secure_download_mode
|
||||||
|
self._port = rom_loader._port
|
||||||
|
self._trace_enabled = rom_loader._trace_enabled
|
||||||
|
self.cache = rom_loader.cache
|
||||||
|
self.flush_input() # resets _slip_reader
|
||||||
|
|
||||||
|
|
||||||
|
ESP32C3ROM.STUB_CLASS = ESP32C3StubLoader
|
||||||
190
mixly/tools/python/esptool/targets/esp32c5.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from .esp32c6 import ESP32C6ROM
|
||||||
|
from ..loader import ESPLoader
|
||||||
|
from ..reset import HardReset
|
||||||
|
from ..util import FatalError
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32C5ROM(ESP32C6ROM):
|
||||||
|
CHIP_NAME = "ESP32-C5"
|
||||||
|
IMAGE_CHIP_ID = 23
|
||||||
|
|
||||||
|
EFUSE_BASE = 0x600B4800
|
||||||
|
EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
|
||||||
|
MAC_EFUSE_REG = EFUSE_BASE + 0x044
|
||||||
|
|
||||||
|
EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
|
||||||
|
|
||||||
|
EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
|
||||||
|
EFUSE_PURPOSE_KEY0_SHIFT = 24
|
||||||
|
EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
|
||||||
|
EFUSE_PURPOSE_KEY1_SHIFT = 28
|
||||||
|
EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY2_SHIFT = 0
|
||||||
|
EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY3_SHIFT = 4
|
||||||
|
EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY4_SHIFT = 8
|
||||||
|
EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY5_SHIFT = 12
|
||||||
|
|
||||||
|
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
|
||||||
|
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
|
||||||
|
|
||||||
|
EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
|
||||||
|
EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
|
||||||
|
|
||||||
|
EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
|
||||||
|
EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
|
||||||
|
|
||||||
|
IROM_MAP_START = 0x42000000
|
||||||
|
IROM_MAP_END = 0x42800000
|
||||||
|
DROM_MAP_START = 0x42800000
|
||||||
|
DROM_MAP_END = 0x43000000
|
||||||
|
|
||||||
|
PCR_SYSCLK_CONF_REG = 0x60096110
|
||||||
|
PCR_SYSCLK_XTAL_FREQ_V = 0x7F << 24
|
||||||
|
PCR_SYSCLK_XTAL_FREQ_S = 24
|
||||||
|
|
||||||
|
UARTDEV_BUF_NO = 0x4085F51C # Variable in ROM .bss which indicates the port in use
|
||||||
|
|
||||||
|
# Magic value for ESP32C5
|
||||||
|
CHIP_DETECT_MAGIC_VALUE = [0x1101406F]
|
||||||
|
|
||||||
|
FLASH_FREQUENCY = {
|
||||||
|
"80m": 0xF,
|
||||||
|
"40m": 0x0,
|
||||||
|
"20m": 0x2,
|
||||||
|
}
|
||||||
|
|
||||||
|
MEMORY_MAP = [
|
||||||
|
[0x00000000, 0x00010000, "PADDING"],
|
||||||
|
[0x42800000, 0x43000000, "DROM"],
|
||||||
|
[0x40800000, 0x40860000, "DRAM"],
|
||||||
|
[0x40800000, 0x40860000, "BYTE_ACCESSIBLE"],
|
||||||
|
[0x4003A000, 0x40040000, "DROM_MASK"],
|
||||||
|
[0x40000000, 0x4003A000, "IROM_MASK"],
|
||||||
|
[0x42000000, 0x42800000, "IROM"],
|
||||||
|
[0x40800000, 0x40860000, "IRAM"],
|
||||||
|
[0x50000000, 0x50004000, "RTC_IRAM"],
|
||||||
|
[0x50000000, 0x50004000, "RTC_DRAM"],
|
||||||
|
[0x600FE000, 0x60100000, "MEM_INTERNAL2"],
|
||||||
|
]
|
||||||
|
|
||||||
|
UF2_FAMILY_ID = 0xF71C0343
|
||||||
|
|
||||||
|
EFUSE_MAX_KEY = 5
|
||||||
|
KEY_PURPOSES: Dict[int, str] = {
|
||||||
|
0: "USER/EMPTY",
|
||||||
|
1: "ECDSA_KEY",
|
||||||
|
2: "XTS_AES_256_KEY_1",
|
||||||
|
3: "XTS_AES_256_KEY_2",
|
||||||
|
4: "XTS_AES_128_KEY",
|
||||||
|
5: "HMAC_DOWN_ALL",
|
||||||
|
6: "HMAC_DOWN_JTAG",
|
||||||
|
7: "HMAC_DOWN_DIGITAL_SIGNATURE",
|
||||||
|
8: "HMAC_UP",
|
||||||
|
9: "SECURE_BOOT_DIGEST0",
|
||||||
|
10: "SECURE_BOOT_DIGEST1",
|
||||||
|
11: "SECURE_BOOT_DIGEST2",
|
||||||
|
12: "KM_INIT_KEY",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_pkg_version(self):
|
||||||
|
num_word = 2
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 26) & 0x07
|
||||||
|
|
||||||
|
def get_minor_chip_version(self):
|
||||||
|
num_word = 2
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x0F
|
||||||
|
|
||||||
|
def get_major_chip_version(self):
|
||||||
|
num_word = 2
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 4) & 0x03
|
||||||
|
|
||||||
|
def get_chip_description(self):
|
||||||
|
chip_name = {
|
||||||
|
0: "ESP32-C5",
|
||||||
|
}.get(self.get_pkg_version(), "unknown ESP32-C5")
|
||||||
|
major_rev = self.get_major_chip_version()
|
||||||
|
minor_rev = self.get_minor_chip_version()
|
||||||
|
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
|
||||||
|
|
||||||
|
def get_crystal_freq(self):
|
||||||
|
# The crystal detection algorithm of ESP32/ESP8266
|
||||||
|
# works for ESP32-C5 as well.
|
||||||
|
return ESPLoader.get_crystal_freq(self)
|
||||||
|
|
||||||
|
def get_crystal_freq_rom_expect(self):
|
||||||
|
return (
|
||||||
|
self.read_reg(self.PCR_SYSCLK_CONF_REG) & self.PCR_SYSCLK_XTAL_FREQ_V
|
||||||
|
) >> self.PCR_SYSCLK_XTAL_FREQ_S
|
||||||
|
|
||||||
|
def hard_reset(self):
|
||||||
|
print("Hard resetting via RTS pin...")
|
||||||
|
HardReset(self._port, self.uses_usb_jtag_serial())()
|
||||||
|
|
||||||
|
def change_baud(self, baud):
|
||||||
|
if not self.IS_STUB:
|
||||||
|
crystal_freq_rom_expect = self.get_crystal_freq_rom_expect()
|
||||||
|
crystal_freq_detect = self.get_crystal_freq()
|
||||||
|
print(
|
||||||
|
f"ROM expects crystal freq: {crystal_freq_rom_expect} MHz, detected {crystal_freq_detect} MHz"
|
||||||
|
)
|
||||||
|
baud_rate = baud
|
||||||
|
# If detect the XTAL is 48MHz, but the ROM code expects it to be 40MHz
|
||||||
|
if crystal_freq_detect == 48 and crystal_freq_rom_expect == 40:
|
||||||
|
baud_rate = baud * 40 // 48
|
||||||
|
# If detect the XTAL is 40MHz, but the ROM code expects it to be 48MHz
|
||||||
|
elif crystal_freq_detect == 40 and crystal_freq_rom_expect == 48:
|
||||||
|
baud_rate = baud * 48 // 40
|
||||||
|
else:
|
||||||
|
ESPLoader.change_baud(self, baud_rate)
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Changing baud rate to {baud_rate}")
|
||||||
|
self.command(self.ESP_CHANGE_BAUDRATE, struct.pack("<II", baud_rate, 0))
|
||||||
|
print("Changed.")
|
||||||
|
self._set_port_baudrate(baud)
|
||||||
|
time.sleep(0.05) # get rid of garbage sent during baud rate change
|
||||||
|
self.flush_input()
|
||||||
|
else:
|
||||||
|
ESPLoader.change_baud(self, baud)
|
||||||
|
|
||||||
|
def check_spi_connection(self, spi_connection):
|
||||||
|
if not set(spi_connection).issubset(set(range(0, 29))):
|
||||||
|
raise FatalError("SPI Pin numbers must be in the range 0-28.")
|
||||||
|
if any([v for v in spi_connection if v in [13, 14]]):
|
||||||
|
print(
|
||||||
|
"WARNING: GPIO pins 13 and 14 are used by USB-Serial/JTAG, "
|
||||||
|
"consider using other pins for SPI flash connection."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32C5StubLoader(ESP32C5ROM):
|
||||||
|
"""Access class for ESP32C5 stub loader, runs on top of ROM.
|
||||||
|
|
||||||
|
(Basically the same as ESP32StubLoader, but different base class.
|
||||||
|
Can possibly be made into a mixin.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
|
||||||
|
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
|
||||||
|
IS_STUB = True
|
||||||
|
|
||||||
|
def __init__(self, rom_loader):
|
||||||
|
self.secure_download_mode = rom_loader.secure_download_mode
|
||||||
|
self._port = rom_loader._port
|
||||||
|
self._trace_enabled = rom_loader._trace_enabled
|
||||||
|
self.cache = rom_loader.cache
|
||||||
|
self.flush_input() # resets _slip_reader
|
||||||
|
|
||||||
|
|
||||||
|
ESP32C5ROM.STUB_CLASS = ESP32C5StubLoader
|
||||||
129
mixly/tools/python/esptool/targets/esp32c5beta3.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from .esp32c6 import ESP32C6ROM
|
||||||
|
from ..loader import ESPLoader
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32C5BETA3ROM(ESP32C6ROM):
|
||||||
|
CHIP_NAME = "ESP32-C5(beta3)"
|
||||||
|
IMAGE_CHIP_ID = 17
|
||||||
|
|
||||||
|
IROM_MAP_START = 0x41000000
|
||||||
|
IROM_MAP_END = 0x41800000
|
||||||
|
DROM_MAP_START = 0x41000000
|
||||||
|
DROM_MAP_END = 0x41800000
|
||||||
|
|
||||||
|
# Magic value for ESP32C5(beta3)
|
||||||
|
CHIP_DETECT_MAGIC_VALUE = [0xE10D8082]
|
||||||
|
|
||||||
|
FLASH_FREQUENCY = {
|
||||||
|
"80m": 0xF,
|
||||||
|
"40m": 0x0,
|
||||||
|
"20m": 0x2,
|
||||||
|
}
|
||||||
|
|
||||||
|
MEMORY_MAP = [
|
||||||
|
[0x00000000, 0x00010000, "PADDING"],
|
||||||
|
[0x41800000, 0x42000000, "DROM"],
|
||||||
|
[0x40800000, 0x40880000, "DRAM"],
|
||||||
|
[0x40800000, 0x40880000, "BYTE_ACCESSIBLE"],
|
||||||
|
[0x4004A000, 0x40050000, "DROM_MASK"],
|
||||||
|
[0x40000000, 0x4004A000, "IROM_MASK"],
|
||||||
|
[0x41000000, 0x41800000, "IROM"],
|
||||||
|
[0x40800000, 0x40880000, "IRAM"],
|
||||||
|
[0x50000000, 0x50004000, "RTC_IRAM"],
|
||||||
|
[0x50000000, 0x50004000, "RTC_DRAM"],
|
||||||
|
[0x600FE000, 0x60100000, "MEM_INTERNAL2"],
|
||||||
|
]
|
||||||
|
|
||||||
|
EFUSE_MAX_KEY = 5
|
||||||
|
KEY_PURPOSES: Dict[int, str] = {
|
||||||
|
0: "USER/EMPTY",
|
||||||
|
1: "ECDSA_KEY",
|
||||||
|
2: "XTS_AES_256_KEY_1",
|
||||||
|
3: "XTS_AES_256_KEY_2",
|
||||||
|
4: "XTS_AES_128_KEY",
|
||||||
|
5: "HMAC_DOWN_ALL",
|
||||||
|
6: "HMAC_DOWN_JTAG",
|
||||||
|
7: "HMAC_DOWN_DIGITAL_SIGNATURE",
|
||||||
|
8: "HMAC_UP",
|
||||||
|
9: "SECURE_BOOT_DIGEST0",
|
||||||
|
10: "SECURE_BOOT_DIGEST1",
|
||||||
|
11: "SECURE_BOOT_DIGEST2",
|
||||||
|
12: "KM_INIT_KEY",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_pkg_version(self):
|
||||||
|
num_word = 2
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 26) & 0x07
|
||||||
|
|
||||||
|
def get_minor_chip_version(self):
|
||||||
|
num_word = 2
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x0F
|
||||||
|
|
||||||
|
def get_major_chip_version(self):
|
||||||
|
num_word = 2
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 4) & 0x03
|
||||||
|
|
||||||
|
def get_chip_description(self):
|
||||||
|
chip_name = {
|
||||||
|
0: "ESP32-C5 beta3 (QFN40)",
|
||||||
|
}.get(self.get_pkg_version(), "unknown ESP32-C5 beta3")
|
||||||
|
major_rev = self.get_major_chip_version()
|
||||||
|
minor_rev = self.get_minor_chip_version()
|
||||||
|
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
|
||||||
|
|
||||||
|
def get_crystal_freq(self):
|
||||||
|
# The crystal detection algorithm of ESP32/ESP8266
|
||||||
|
# works for ESP32-C5 beta3 as well.
|
||||||
|
return ESPLoader.get_crystal_freq(self)
|
||||||
|
|
||||||
|
def change_baud(self, baud):
|
||||||
|
rom_with_48M_XTAL = not self.IS_STUB and self.get_crystal_freq() == 48
|
||||||
|
if rom_with_48M_XTAL:
|
||||||
|
# The code is copied over from ESPLoader.change_baud().
|
||||||
|
# Probably this is just a temporary solution until the next chip revision.
|
||||||
|
|
||||||
|
# The ROM code thinks it uses a 40 MHz XTAL. Recompute the baud rate
|
||||||
|
# in order to trick the ROM code to set the correct baud rate for
|
||||||
|
# a 48 MHz XTAL.
|
||||||
|
false_rom_baud = baud * 40 // 48
|
||||||
|
|
||||||
|
print(f"Changing baud rate to {baud}")
|
||||||
|
self.command(
|
||||||
|
self.ESP_CHANGE_BAUDRATE, struct.pack("<II", false_rom_baud, 0)
|
||||||
|
)
|
||||||
|
print("Changed.")
|
||||||
|
self._set_port_baudrate(baud)
|
||||||
|
time.sleep(0.05) # get rid of garbage sent during baud rate change
|
||||||
|
self.flush_input()
|
||||||
|
else:
|
||||||
|
ESPLoader.change_baud(self, baud)
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32C5BETA3StubLoader(ESP32C5BETA3ROM):
|
||||||
|
"""Access class for ESP32C5BETA3 stub loader, runs on top of ROM.
|
||||||
|
|
||||||
|
(Basically the same as ESP32StubLoader, but different base class.
|
||||||
|
Can possibly be made into a mixin.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
|
||||||
|
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
|
||||||
|
IS_STUB = True
|
||||||
|
|
||||||
|
def __init__(self, rom_loader):
|
||||||
|
self.secure_download_mode = rom_loader.secure_download_mode
|
||||||
|
self._port = rom_loader._port
|
||||||
|
self._trace_enabled = rom_loader._trace_enabled
|
||||||
|
self.cache = rom_loader.cache
|
||||||
|
self.flush_input() # resets _slip_reader
|
||||||
|
|
||||||
|
|
||||||
|
ESP32C5BETA3ROM.STUB_CLASS = ESP32C5BETA3StubLoader
|
||||||
216
mixly/tools/python/esptool/targets/esp32c6.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2022 Fredrik Ahlberg, Angus Gratton,
|
||||||
|
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from .esp32c3 import ESP32C3ROM
|
||||||
|
from ..util import FatalError, NotImplementedInROMError
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32C6ROM(ESP32C3ROM):
|
||||||
|
CHIP_NAME = "ESP32-C6"
|
||||||
|
IMAGE_CHIP_ID = 13
|
||||||
|
|
||||||
|
IROM_MAP_START = 0x42000000
|
||||||
|
IROM_MAP_END = 0x42800000
|
||||||
|
DROM_MAP_START = 0x42800000
|
||||||
|
DROM_MAP_END = 0x43000000
|
||||||
|
|
||||||
|
BOOTLOADER_FLASH_OFFSET = 0x0
|
||||||
|
|
||||||
|
# Magic value for ESP32C6
|
||||||
|
CHIP_DETECT_MAGIC_VALUE = [0x2CE0806F]
|
||||||
|
|
||||||
|
SPI_REG_BASE = 0x60003000
|
||||||
|
SPI_USR_OFFS = 0x18
|
||||||
|
SPI_USR1_OFFS = 0x1C
|
||||||
|
SPI_USR2_OFFS = 0x20
|
||||||
|
SPI_MOSI_DLEN_OFFS = 0x24
|
||||||
|
SPI_MISO_DLEN_OFFS = 0x28
|
||||||
|
SPI_W0_OFFS = 0x58
|
||||||
|
|
||||||
|
UART_DATE_REG_ADDR = 0x60000000 + 0x7C
|
||||||
|
|
||||||
|
EFUSE_BASE = 0x600B0800
|
||||||
|
EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
|
||||||
|
MAC_EFUSE_REG = EFUSE_BASE + 0x044
|
||||||
|
|
||||||
|
EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
|
||||||
|
|
||||||
|
EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
|
||||||
|
EFUSE_PURPOSE_KEY0_SHIFT = 24
|
||||||
|
EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
|
||||||
|
EFUSE_PURPOSE_KEY1_SHIFT = 28
|
||||||
|
EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY2_SHIFT = 0
|
||||||
|
EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY3_SHIFT = 4
|
||||||
|
EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY4_SHIFT = 8
|
||||||
|
EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY5_SHIFT = 12
|
||||||
|
|
||||||
|
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
|
||||||
|
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
|
||||||
|
|
||||||
|
EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
|
||||||
|
EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
|
||||||
|
|
||||||
|
EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
|
||||||
|
EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
|
||||||
|
|
||||||
|
PURPOSE_VAL_XTS_AES128_KEY = 4
|
||||||
|
|
||||||
|
SUPPORTS_ENCRYPTED_FLASH = True
|
||||||
|
|
||||||
|
FLASH_ENCRYPTED_WRITE_ALIGN = 16
|
||||||
|
|
||||||
|
UARTDEV_BUF_NO = 0x4087F580 # Variable in ROM .bss which indicates the port in use
|
||||||
|
UARTDEV_BUF_NO_USB_JTAG_SERIAL = 3 # The above var when USB-JTAG/Serial is used
|
||||||
|
|
||||||
|
DR_REG_LP_WDT_BASE = 0x600B1C00
|
||||||
|
RTC_CNTL_WDTCONFIG0_REG = DR_REG_LP_WDT_BASE + 0x0 # LP_WDT_RWDT_CONFIG0_REG
|
||||||
|
RTC_CNTL_WDTWPROTECT_REG = DR_REG_LP_WDT_BASE + 0x0018 # LP_WDT_RWDT_WPROTECT_REG
|
||||||
|
|
||||||
|
RTC_CNTL_SWD_CONF_REG = DR_REG_LP_WDT_BASE + 0x001C # LP_WDT_SWD_CONFIG_REG
|
||||||
|
RTC_CNTL_SWD_AUTO_FEED_EN = 1 << 18
|
||||||
|
RTC_CNTL_SWD_WPROTECT_REG = DR_REG_LP_WDT_BASE + 0x0020 # LP_WDT_SWD_WPROTECT_REG
|
||||||
|
RTC_CNTL_SWD_WKEY = 0x50D83AA1 # LP_WDT_SWD_WKEY, same as WDT key in this case
|
||||||
|
|
||||||
|
FLASH_FREQUENCY = {
|
||||||
|
"80m": 0x0, # workaround for wrong mspi HS div value in ROM
|
||||||
|
"40m": 0x0,
|
||||||
|
"20m": 0x2,
|
||||||
|
}
|
||||||
|
|
||||||
|
MEMORY_MAP = [
|
||||||
|
[0x00000000, 0x00010000, "PADDING"],
|
||||||
|
[0x42800000, 0x43000000, "DROM"],
|
||||||
|
[0x40800000, 0x40880000, "DRAM"],
|
||||||
|
[0x40800000, 0x40880000, "BYTE_ACCESSIBLE"],
|
||||||
|
[0x4004AC00, 0x40050000, "DROM_MASK"],
|
||||||
|
[0x40000000, 0x4004AC00, "IROM_MASK"],
|
||||||
|
[0x42000000, 0x42800000, "IROM"],
|
||||||
|
[0x40800000, 0x40880000, "IRAM"],
|
||||||
|
[0x50000000, 0x50004000, "RTC_IRAM"],
|
||||||
|
[0x50000000, 0x50004000, "RTC_DRAM"],
|
||||||
|
[0x600FE000, 0x60100000, "MEM_INTERNAL2"],
|
||||||
|
]
|
||||||
|
|
||||||
|
UF2_FAMILY_ID = 0x540DDF62
|
||||||
|
|
||||||
|
def get_pkg_version(self):
|
||||||
|
num_word = 3
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x07
|
||||||
|
|
||||||
|
def get_minor_chip_version(self):
|
||||||
|
num_word = 3
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x0F
|
||||||
|
|
||||||
|
def get_major_chip_version(self):
|
||||||
|
num_word = 3
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 22) & 0x03
|
||||||
|
|
||||||
|
def get_chip_description(self):
|
||||||
|
chip_name = {
|
||||||
|
0: "ESP32-C6 (QFN40)",
|
||||||
|
1: "ESP32-C6FH4 (QFN32)",
|
||||||
|
}.get(self.get_pkg_version(), "unknown ESP32-C6")
|
||||||
|
major_rev = self.get_major_chip_version()
|
||||||
|
minor_rev = self.get_minor_chip_version()
|
||||||
|
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
|
||||||
|
|
||||||
|
def get_chip_features(self):
|
||||||
|
return ["WiFi 6", "BT 5", "IEEE802.15.4"]
|
||||||
|
|
||||||
|
def get_crystal_freq(self):
|
||||||
|
# ESP32C6 XTAL is fixed to 40MHz
|
||||||
|
return 40
|
||||||
|
|
||||||
|
def override_vddsdio(self, new_voltage):
|
||||||
|
raise NotImplementedInROMError(
|
||||||
|
"VDD_SDIO overrides are not supported for ESP32-C6"
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_mac(self, mac_type="BASE_MAC"):
|
||||||
|
"""Read MAC from EFUSE region"""
|
||||||
|
mac0 = self.read_reg(self.MAC_EFUSE_REG)
|
||||||
|
mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
|
||||||
|
base_mac = struct.pack(">II", mac1, mac0)[2:]
|
||||||
|
ext_mac = struct.pack(">H", (mac1 >> 16) & 0xFFFF)
|
||||||
|
eui64 = base_mac[0:3] + ext_mac + base_mac[3:6]
|
||||||
|
# BASE MAC: 60:55:f9:f7:2c:a2
|
||||||
|
# EUI64 MAC: 60:55:f9:ff:fe:f7:2c:a2
|
||||||
|
# EXT_MAC: ff:fe
|
||||||
|
macs = {
|
||||||
|
"BASE_MAC": tuple(base_mac),
|
||||||
|
"EUI64": tuple(eui64),
|
||||||
|
"MAC_EXT": tuple(ext_mac),
|
||||||
|
}
|
||||||
|
return macs.get(mac_type, None)
|
||||||
|
|
||||||
|
def get_flash_crypt_config(self):
|
||||||
|
return None # doesn't exist on ESP32-C6
|
||||||
|
|
||||||
|
def get_secure_boot_enabled(self):
|
||||||
|
return (
|
||||||
|
self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
|
||||||
|
& self.EFUSE_SECURE_BOOT_EN_MASK
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_key_block_purpose(self, key_block):
|
||||||
|
if key_block < 0 or key_block > self.EFUSE_MAX_KEY:
|
||||||
|
raise FatalError(
|
||||||
|
f"Valid key block numbers must be in range 0-{self.EFUSE_MAX_KEY}"
|
||||||
|
)
|
||||||
|
|
||||||
|
reg, shift = [
|
||||||
|
(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
|
||||||
|
][key_block]
|
||||||
|
return (self.read_reg(reg) >> shift) & 0xF
|
||||||
|
|
||||||
|
def is_flash_encryption_key_valid(self):
|
||||||
|
# Need to see an AES-128 key
|
||||||
|
purposes = [
|
||||||
|
self.get_key_block_purpose(b) for b in range(self.EFUSE_MAX_KEY + 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes)
|
||||||
|
|
||||||
|
def check_spi_connection(self, spi_connection):
|
||||||
|
if not set(spi_connection).issubset(set(range(0, 31))):
|
||||||
|
raise FatalError("SPI Pin numbers must be in the range 0-30.")
|
||||||
|
if any([v for v in spi_connection if v in [12, 13]]):
|
||||||
|
print(
|
||||||
|
"WARNING: GPIO pins 12 and 13 are used by USB-Serial/JTAG, "
|
||||||
|
"consider using other pins for SPI flash connection."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32C6StubLoader(ESP32C6ROM):
|
||||||
|
"""Access class for ESP32C6 stub loader, runs on top of ROM.
|
||||||
|
|
||||||
|
(Basically the same as ESP32StubLoader, but different base class.
|
||||||
|
Can possibly be made into a mixin.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
|
||||||
|
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
|
||||||
|
IS_STUB = True
|
||||||
|
|
||||||
|
def __init__(self, rom_loader):
|
||||||
|
self.secure_download_mode = rom_loader.secure_download_mode
|
||||||
|
self._port = rom_loader._port
|
||||||
|
self._trace_enabled = rom_loader._trace_enabled
|
||||||
|
self.cache = rom_loader.cache
|
||||||
|
self.flush_input() # resets _slip_reader
|
||||||
|
|
||||||
|
|
||||||
|
ESP32C6ROM.STUB_CLASS = ESP32C6StubLoader
|
||||||
144
mixly/tools/python/esptool/targets/esp32c61.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import struct
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from .esp32c6 import ESP32C6ROM
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32C61ROM(ESP32C6ROM):
|
||||||
|
CHIP_NAME = "ESP32-C61"
|
||||||
|
IMAGE_CHIP_ID = 20
|
||||||
|
|
||||||
|
# Magic value for ESP32C61
|
||||||
|
CHIP_DETECT_MAGIC_VALUE = [0x33F0206F, 0x2421606F]
|
||||||
|
|
||||||
|
UART_DATE_REG_ADDR = 0x60000000 + 0x7C
|
||||||
|
|
||||||
|
EFUSE_BASE = 0x600B4800
|
||||||
|
EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
|
||||||
|
MAC_EFUSE_REG = EFUSE_BASE + 0x044
|
||||||
|
|
||||||
|
EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
|
||||||
|
|
||||||
|
EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
|
||||||
|
EFUSE_PURPOSE_KEY0_SHIFT = 0
|
||||||
|
EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
|
||||||
|
EFUSE_PURPOSE_KEY1_SHIFT = 4
|
||||||
|
EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x34
|
||||||
|
EFUSE_PURPOSE_KEY2_SHIFT = 8
|
||||||
|
EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x34
|
||||||
|
EFUSE_PURPOSE_KEY3_SHIFT = 12
|
||||||
|
EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x34
|
||||||
|
EFUSE_PURPOSE_KEY4_SHIFT = 16
|
||||||
|
EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x34
|
||||||
|
EFUSE_PURPOSE_KEY5_SHIFT = 20
|
||||||
|
|
||||||
|
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
|
||||||
|
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
|
||||||
|
|
||||||
|
EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x030
|
||||||
|
EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 23
|
||||||
|
|
||||||
|
EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x034
|
||||||
|
EFUSE_SECURE_BOOT_EN_MASK = 1 << 26
|
||||||
|
|
||||||
|
FLASH_FREQUENCY = {
|
||||||
|
"80m": 0xF,
|
||||||
|
"40m": 0x0,
|
||||||
|
"20m": 0x2,
|
||||||
|
}
|
||||||
|
|
||||||
|
MEMORY_MAP = [
|
||||||
|
[0x00000000, 0x00010000, "PADDING"],
|
||||||
|
[0x41800000, 0x42000000, "DROM"],
|
||||||
|
[0x40800000, 0x40860000, "DRAM"],
|
||||||
|
[0x40800000, 0x40860000, "BYTE_ACCESSIBLE"],
|
||||||
|
[0x4004AC00, 0x40050000, "DROM_MASK"],
|
||||||
|
[0x40000000, 0x4004AC00, "IROM_MASK"],
|
||||||
|
[0x41000000, 0x41800000, "IROM"],
|
||||||
|
[0x40800000, 0x40860000, "IRAM"],
|
||||||
|
[0x50000000, 0x50004000, "RTC_IRAM"],
|
||||||
|
[0x50000000, 0x50004000, "RTC_DRAM"],
|
||||||
|
[0x600FE000, 0x60100000, "MEM_INTERNAL2"],
|
||||||
|
]
|
||||||
|
|
||||||
|
UF2_FAMILY_ID = 0x77D850C4
|
||||||
|
|
||||||
|
EFUSE_MAX_KEY = 5
|
||||||
|
KEY_PURPOSES: Dict[int, str] = {
|
||||||
|
0: "USER/EMPTY",
|
||||||
|
1: "ECDSA_KEY",
|
||||||
|
2: "XTS_AES_256_KEY_1",
|
||||||
|
3: "XTS_AES_256_KEY_2",
|
||||||
|
4: "XTS_AES_128_KEY",
|
||||||
|
5: "HMAC_DOWN_ALL",
|
||||||
|
6: "HMAC_DOWN_JTAG",
|
||||||
|
7: "HMAC_DOWN_DIGITAL_SIGNATURE",
|
||||||
|
8: "HMAC_UP",
|
||||||
|
9: "SECURE_BOOT_DIGEST0",
|
||||||
|
10: "SECURE_BOOT_DIGEST1",
|
||||||
|
11: "SECURE_BOOT_DIGEST2",
|
||||||
|
12: "KM_INIT_KEY",
|
||||||
|
13: "XTS_AES_256_KEY_1_PSRAM",
|
||||||
|
14: "XTS_AES_256_KEY_2_PSRAM",
|
||||||
|
15: "XTS_AES_128_KEY_PSRAM",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_pkg_version(self):
|
||||||
|
num_word = 2
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 26) & 0x07
|
||||||
|
|
||||||
|
def get_minor_chip_version(self):
|
||||||
|
num_word = 2
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x0F
|
||||||
|
|
||||||
|
def get_major_chip_version(self):
|
||||||
|
num_word = 2
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 4) & 0x03
|
||||||
|
|
||||||
|
def get_chip_description(self):
|
||||||
|
chip_name = {
|
||||||
|
0: "ESP32-C61",
|
||||||
|
}.get(self.get_pkg_version(), "unknown ESP32-C61")
|
||||||
|
major_rev = self.get_major_chip_version()
|
||||||
|
minor_rev = self.get_minor_chip_version()
|
||||||
|
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
|
||||||
|
|
||||||
|
def get_chip_features(self):
|
||||||
|
return ["WiFi 6", "BT 5"]
|
||||||
|
|
||||||
|
def read_mac(self, mac_type="BASE_MAC"):
|
||||||
|
"""Read MAC from EFUSE region"""
|
||||||
|
mac0 = self.read_reg(self.MAC_EFUSE_REG)
|
||||||
|
mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
|
||||||
|
base_mac = struct.pack(">II", mac1, mac0)[2:]
|
||||||
|
# BASE MAC: 60:55:f9:f7:2c:a2
|
||||||
|
macs = {
|
||||||
|
"BASE_MAC": tuple(base_mac),
|
||||||
|
}
|
||||||
|
return macs.get(mac_type, None)
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32C61StubLoader(ESP32C61ROM):
|
||||||
|
"""Access class for ESP32C61 stub loader, runs on top of ROM.
|
||||||
|
|
||||||
|
(Basically the same as ESP32StubLoader, but different base class.
|
||||||
|
Can possibly be made into a mixin.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
|
||||||
|
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
|
||||||
|
IS_STUB = True
|
||||||
|
|
||||||
|
def __init__(self, rom_loader):
|
||||||
|
self.secure_download_mode = rom_loader.secure_download_mode
|
||||||
|
self._port = rom_loader._port
|
||||||
|
self._trace_enabled = rom_loader._trace_enabled
|
||||||
|
self.cache = rom_loader.cache
|
||||||
|
self.flush_input() # resets _slip_reader
|
||||||
|
|
||||||
|
|
||||||
|
ESP32C61ROM.STUB_CLASS = ESP32C61StubLoader
|
||||||
27
mixly/tools/python/esptool/targets/esp32c6beta.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
|
||||||
|
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
from .esp32c3 import ESP32C3ROM
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32C6BETAROM(ESP32C3ROM):
|
||||||
|
CHIP_NAME = "ESP32-C6(beta)"
|
||||||
|
IMAGE_CHIP_ID = 7
|
||||||
|
|
||||||
|
CHIP_DETECT_MAGIC_VALUE = [0x0DA1806F]
|
||||||
|
|
||||||
|
UART_DATE_REG_ADDR = 0x00000500
|
||||||
|
|
||||||
|
def get_chip_description(self):
|
||||||
|
chip_name = {
|
||||||
|
0: "ESP32-C6 (QFN40)",
|
||||||
|
1: "ESP32-C6FH4 (QFN32)",
|
||||||
|
}.get(self.get_pkg_version(), "unknown ESP32-C6")
|
||||||
|
major_rev = self.get_major_chip_version()
|
||||||
|
minor_rev = self.get_minor_chip_version()
|
||||||
|
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
|
||||||
|
|
||||||
|
def _post_connect(self):
|
||||||
|
pass
|
||||||
109
mixly/tools/python/esptool/targets/esp32h2.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2022 Fredrik Ahlberg, Angus Gratton,
|
||||||
|
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from .esp32c6 import ESP32C6ROM
|
||||||
|
from ..util import FatalError
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32H2ROM(ESP32C6ROM):
|
||||||
|
CHIP_NAME = "ESP32-H2"
|
||||||
|
IMAGE_CHIP_ID = 16
|
||||||
|
|
||||||
|
# Magic value for ESP32H2
|
||||||
|
CHIP_DETECT_MAGIC_VALUE = [0xD7B73E80]
|
||||||
|
|
||||||
|
DR_REG_LP_WDT_BASE = 0x600B1C00
|
||||||
|
RTC_CNTL_WDTCONFIG0_REG = DR_REG_LP_WDT_BASE + 0x0 # LP_WDT_RWDT_CONFIG0_REG
|
||||||
|
RTC_CNTL_WDTWPROTECT_REG = DR_REG_LP_WDT_BASE + 0x001C # LP_WDT_RWDT_WPROTECT_REG
|
||||||
|
|
||||||
|
RTC_CNTL_SWD_CONF_REG = DR_REG_LP_WDT_BASE + 0x0020 # LP_WDT_SWD_CONFIG_REG
|
||||||
|
RTC_CNTL_SWD_AUTO_FEED_EN = 1 << 18
|
||||||
|
RTC_CNTL_SWD_WPROTECT_REG = DR_REG_LP_WDT_BASE + 0x0024 # LP_WDT_SWD_WPROTECT_REG
|
||||||
|
RTC_CNTL_SWD_WKEY = 0x50D83AA1 # LP_WDT_SWD_WKEY, same as WDT key in this case
|
||||||
|
|
||||||
|
FLASH_FREQUENCY = {
|
||||||
|
"48m": 0xF,
|
||||||
|
"24m": 0x0,
|
||||||
|
"16m": 0x1,
|
||||||
|
"12m": 0x2,
|
||||||
|
}
|
||||||
|
|
||||||
|
UF2_FAMILY_ID = 0x332726F6
|
||||||
|
|
||||||
|
EFUSE_MAX_KEY = 5
|
||||||
|
KEY_PURPOSES: Dict[int, str] = {
|
||||||
|
0: "USER/EMPTY",
|
||||||
|
1: "ECDSA_KEY",
|
||||||
|
2: "XTS_AES_256_KEY_1",
|
||||||
|
3: "XTS_AES_256_KEY_2",
|
||||||
|
4: "XTS_AES_128_KEY",
|
||||||
|
5: "HMAC_DOWN_ALL",
|
||||||
|
6: "HMAC_DOWN_JTAG",
|
||||||
|
7: "HMAC_DOWN_DIGITAL_SIGNATURE",
|
||||||
|
8: "HMAC_UP",
|
||||||
|
9: "SECURE_BOOT_DIGEST0",
|
||||||
|
10: "SECURE_BOOT_DIGEST1",
|
||||||
|
11: "SECURE_BOOT_DIGEST2",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_pkg_version(self):
|
||||||
|
num_word = 4
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x07
|
||||||
|
|
||||||
|
def get_minor_chip_version(self):
|
||||||
|
num_word = 3
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x07
|
||||||
|
|
||||||
|
def get_major_chip_version(self):
|
||||||
|
num_word = 3
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x03
|
||||||
|
|
||||||
|
def get_chip_description(self):
|
||||||
|
chip_name = {
|
||||||
|
0: "ESP32-H2",
|
||||||
|
}.get(self.get_pkg_version(), "unknown ESP32-H2")
|
||||||
|
major_rev = self.get_major_chip_version()
|
||||||
|
minor_rev = self.get_minor_chip_version()
|
||||||
|
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
|
||||||
|
|
||||||
|
def get_chip_features(self):
|
||||||
|
return ["BLE", "IEEE802.15.4"]
|
||||||
|
|
||||||
|
def get_crystal_freq(self):
|
||||||
|
# ESP32H2 XTAL is fixed to 32MHz
|
||||||
|
return 32
|
||||||
|
|
||||||
|
def check_spi_connection(self, spi_connection):
|
||||||
|
if not set(spi_connection).issubset(set(range(0, 28))):
|
||||||
|
raise FatalError("SPI Pin numbers must be in the range 0-27.")
|
||||||
|
if any([v for v in spi_connection if v in [26, 27]]):
|
||||||
|
print(
|
||||||
|
"WARNING: GPIO pins 26 and 27 are used by USB-Serial/JTAG, "
|
||||||
|
"consider using other pins for SPI flash connection."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32H2StubLoader(ESP32H2ROM):
|
||||||
|
"""Access class for ESP32H2 stub loader, runs on top of ROM.
|
||||||
|
|
||||||
|
(Basically the same as ESP32StubLoader, but different base class.
|
||||||
|
Can possibly be made into a mixin.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
|
||||||
|
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
|
||||||
|
IS_STUB = True
|
||||||
|
|
||||||
|
def __init__(self, rom_loader):
|
||||||
|
self.secure_download_mode = rom_loader.secure_download_mode
|
||||||
|
self._port = rom_loader._port
|
||||||
|
self._trace_enabled = rom_loader._trace_enabled
|
||||||
|
self.cache = rom_loader.cache
|
||||||
|
self.flush_input() # resets _slip_reader
|
||||||
|
|
||||||
|
|
||||||
|
ESP32H2ROM.STUB_CLASS = ESP32H2StubLoader
|
||||||
186
mixly/tools/python/esptool/targets/esp32h2beta1.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
|
||||||
|
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import struct
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from .esp32c3 import ESP32C3ROM
|
||||||
|
from ..util import FatalError, NotImplementedInROMError
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32H2BETA1ROM(ESP32C3ROM):
|
||||||
|
CHIP_NAME = "ESP32-H2(beta1)"
|
||||||
|
IMAGE_CHIP_ID = 10
|
||||||
|
|
||||||
|
IROM_MAP_START = 0x42000000
|
||||||
|
IROM_MAP_END = 0x42800000
|
||||||
|
DROM_MAP_START = 0x3C000000
|
||||||
|
DROM_MAP_END = 0x3C800000
|
||||||
|
|
||||||
|
SPI_REG_BASE = 0x60002000
|
||||||
|
SPI_USR_OFFS = 0x18
|
||||||
|
SPI_USR1_OFFS = 0x1C
|
||||||
|
SPI_USR2_OFFS = 0x20
|
||||||
|
SPI_MOSI_DLEN_OFFS = 0x24
|
||||||
|
SPI_MISO_DLEN_OFFS = 0x28
|
||||||
|
SPI_W0_OFFS = 0x58
|
||||||
|
|
||||||
|
BOOTLOADER_FLASH_OFFSET = 0x0
|
||||||
|
|
||||||
|
CHIP_DETECT_MAGIC_VALUE = [0xCA26CC22]
|
||||||
|
|
||||||
|
UART_DATE_REG_ADDR = 0x60000000 + 0x7C
|
||||||
|
|
||||||
|
EFUSE_BASE = 0x6001A000
|
||||||
|
EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
|
||||||
|
MAC_EFUSE_REG = EFUSE_BASE + 0x044
|
||||||
|
|
||||||
|
EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
|
||||||
|
|
||||||
|
EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
|
||||||
|
EFUSE_PURPOSE_KEY0_SHIFT = 24
|
||||||
|
EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
|
||||||
|
EFUSE_PURPOSE_KEY1_SHIFT = 28
|
||||||
|
EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY2_SHIFT = 0
|
||||||
|
EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY3_SHIFT = 4
|
||||||
|
EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY4_SHIFT = 8
|
||||||
|
EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY5_SHIFT = 12
|
||||||
|
|
||||||
|
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
|
||||||
|
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
|
||||||
|
|
||||||
|
EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
|
||||||
|
EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
|
||||||
|
|
||||||
|
EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
|
||||||
|
EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
|
||||||
|
|
||||||
|
PURPOSE_VAL_XTS_AES128_KEY = 4
|
||||||
|
|
||||||
|
SUPPORTS_ENCRYPTED_FLASH = True
|
||||||
|
|
||||||
|
FLASH_ENCRYPTED_WRITE_ALIGN = 16
|
||||||
|
|
||||||
|
MEMORY_MAP: List = []
|
||||||
|
|
||||||
|
FLASH_FREQUENCY = {
|
||||||
|
"48m": 0xF,
|
||||||
|
"24m": 0x0,
|
||||||
|
"16m": 0x1,
|
||||||
|
"12m": 0x2,
|
||||||
|
}
|
||||||
|
|
||||||
|
EFUSE_MAX_KEY = 5
|
||||||
|
KEY_PURPOSES: Dict[int, str] = {
|
||||||
|
0: "USER/EMPTY",
|
||||||
|
1: "ECDSA_KEY",
|
||||||
|
2: "RESERVED",
|
||||||
|
4: "XTS_AES_128_KEY",
|
||||||
|
5: "HMAC_DOWN_ALL",
|
||||||
|
6: "HMAC_DOWN_JTAG",
|
||||||
|
7: "HMAC_DOWN_DIGITAL_SIGNATURE",
|
||||||
|
8: "HMAC_UP",
|
||||||
|
9: "SECURE_BOOT_DIGEST0",
|
||||||
|
10: "SECURE_BOOT_DIGEST1",
|
||||||
|
11: "SECURE_BOOT_DIGEST2",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_pkg_version(self):
|
||||||
|
num_word = 4
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x07
|
||||||
|
|
||||||
|
def get_minor_chip_version(self):
|
||||||
|
num_word = 3
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x07
|
||||||
|
|
||||||
|
def get_major_chip_version(self):
|
||||||
|
num_word = 3
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x03
|
||||||
|
|
||||||
|
def get_chip_description(self):
|
||||||
|
chip_name = {
|
||||||
|
0: "ESP32-H2",
|
||||||
|
}.get(self.get_pkg_version(), "unknown ESP32-H2")
|
||||||
|
major_rev = self.get_major_chip_version()
|
||||||
|
minor_rev = self.get_minor_chip_version()
|
||||||
|
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
|
||||||
|
|
||||||
|
def get_chip_features(self):
|
||||||
|
return ["BLE", "IEEE802.15.4"]
|
||||||
|
|
||||||
|
def get_crystal_freq(self):
|
||||||
|
return 32
|
||||||
|
|
||||||
|
def override_vddsdio(self, new_voltage):
|
||||||
|
raise NotImplementedInROMError(
|
||||||
|
"VDD_SDIO overrides are not supported for ESP32-H2"
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_mac(self, mac_type="BASE_MAC"):
|
||||||
|
"""Read MAC from EFUSE region"""
|
||||||
|
if mac_type != "BASE_MAC":
|
||||||
|
return None
|
||||||
|
mac0 = self.read_reg(self.MAC_EFUSE_REG)
|
||||||
|
mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
|
||||||
|
bitstring = struct.pack(">II", mac1, mac0)[2:]
|
||||||
|
return tuple(bitstring)
|
||||||
|
|
||||||
|
def get_flash_crypt_config(self):
|
||||||
|
return None # doesn't exist on ESP32-H2
|
||||||
|
|
||||||
|
def get_key_block_purpose(self, key_block):
|
||||||
|
if key_block < 0 or key_block > self.EFUSE_MAX_KEY:
|
||||||
|
raise FatalError(
|
||||||
|
f"Valid key block numbers must be in range 0-{self.EFUSE_MAX_KEY}"
|
||||||
|
)
|
||||||
|
|
||||||
|
reg, shift = [
|
||||||
|
(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
|
||||||
|
][key_block]
|
||||||
|
return (self.read_reg(reg) >> shift) & 0xF
|
||||||
|
|
||||||
|
def is_flash_encryption_key_valid(self):
|
||||||
|
# Need to see an AES-128 key
|
||||||
|
purposes = [
|
||||||
|
self.get_key_block_purpose(b) for b in range(self.EFUSE_MAX_KEY + 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes)
|
||||||
|
|
||||||
|
def _post_connect(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32H2BETA1StubLoader(ESP32H2BETA1ROM):
|
||||||
|
"""Access class for ESP32H2BETA1 stub loader, runs on top of ROM.
|
||||||
|
|
||||||
|
(Basically the same as ESP32StubLoader, but different base class.
|
||||||
|
Can possibly be made into a mixin.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
|
||||||
|
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
|
||||||
|
IS_STUB = True
|
||||||
|
|
||||||
|
def __init__(self, rom_loader):
|
||||||
|
self.secure_download_mode = rom_loader.secure_download_mode
|
||||||
|
self._port = rom_loader._port
|
||||||
|
self._trace_enabled = rom_loader._trace_enabled
|
||||||
|
self.cache = rom_loader.cache
|
||||||
|
self.flush_input() # resets _slip_reader
|
||||||
|
|
||||||
|
|
||||||
|
ESP32H2BETA1ROM.STUB_CLASS = ESP32H2BETA1StubLoader
|
||||||
43
mixly/tools/python/esptool/targets/esp32h2beta2.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton,
|
||||||
|
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
from .esp32h2beta1 import ESP32H2BETA1ROM
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32H2BETA2ROM(ESP32H2BETA1ROM):
|
||||||
|
CHIP_NAME = "ESP32-H2(beta2)"
|
||||||
|
IMAGE_CHIP_ID = 14
|
||||||
|
|
||||||
|
CHIP_DETECT_MAGIC_VALUE = [0x6881B06F]
|
||||||
|
|
||||||
|
def get_chip_description(self):
|
||||||
|
chip_name = {
|
||||||
|
1: "ESP32-H2(beta2)",
|
||||||
|
}.get(self.get_pkg_version(), "unknown ESP32-H2")
|
||||||
|
major_rev = self.get_major_chip_version()
|
||||||
|
minor_rev = self.get_minor_chip_version()
|
||||||
|
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32H2BETA2StubLoader(ESP32H2BETA2ROM):
|
||||||
|
"""Access class for ESP32H2BETA2 stub loader, runs on top of ROM.
|
||||||
|
|
||||||
|
(Basically the same as ESP32StubLoader, but different base class.
|
||||||
|
Can possibly be made into a mixin.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
|
||||||
|
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
|
||||||
|
IS_STUB = True
|
||||||
|
|
||||||
|
def __init__(self, rom_loader):
|
||||||
|
self.secure_download_mode = rom_loader.secure_download_mode
|
||||||
|
self._port = rom_loader._port
|
||||||
|
self._trace_enabled = rom_loader._trace_enabled
|
||||||
|
self.cache = rom_loader.cache
|
||||||
|
self.flush_input() # resets _slip_reader
|
||||||
|
|
||||||
|
|
||||||
|
ESP32H2BETA2ROM.STUB_CLASS = ESP32H2BETA2StubLoader
|
||||||
228
mixly/tools/python/esptool/targets/esp32p4.py
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2023 Fredrik Ahlberg, Angus Gratton,
|
||||||
|
# Espressif Systems (Shanghai) CO LTD, other contributors as noted.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import struct
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from .esp32 import ESP32ROM
|
||||||
|
from ..loader import ESPLoader
|
||||||
|
from ..util import FatalError, NotImplementedInROMError
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32P4ROM(ESP32ROM):
|
||||||
|
CHIP_NAME = "ESP32-P4"
|
||||||
|
IMAGE_CHIP_ID = 18
|
||||||
|
|
||||||
|
IROM_MAP_START = 0x40000000
|
||||||
|
IROM_MAP_END = 0x4C000000
|
||||||
|
DROM_MAP_START = 0x40000000
|
||||||
|
DROM_MAP_END = 0x4C000000
|
||||||
|
|
||||||
|
BOOTLOADER_FLASH_OFFSET = 0x2000 # First 2 sectors are reserved for FE purposes
|
||||||
|
|
||||||
|
CHIP_DETECT_MAGIC_VALUE = [0x0, 0x0ADDBAD0]
|
||||||
|
|
||||||
|
UART_DATE_REG_ADDR = 0x500CA000 + 0x8C
|
||||||
|
|
||||||
|
EFUSE_BASE = 0x5012D000
|
||||||
|
EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
|
||||||
|
MAC_EFUSE_REG = EFUSE_BASE + 0x044
|
||||||
|
|
||||||
|
SPI_REG_BASE = 0x5008D000 # SPIMEM1
|
||||||
|
SPI_USR_OFFS = 0x18
|
||||||
|
SPI_USR1_OFFS = 0x1C
|
||||||
|
SPI_USR2_OFFS = 0x20
|
||||||
|
SPI_MOSI_DLEN_OFFS = 0x24
|
||||||
|
SPI_MISO_DLEN_OFFS = 0x28
|
||||||
|
SPI_W0_OFFS = 0x58
|
||||||
|
|
||||||
|
SPI_ADDR_REG_MSB = False
|
||||||
|
|
||||||
|
EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address
|
||||||
|
|
||||||
|
EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
|
||||||
|
EFUSE_PURPOSE_KEY0_SHIFT = 24
|
||||||
|
EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
|
||||||
|
EFUSE_PURPOSE_KEY1_SHIFT = 28
|
||||||
|
EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY2_SHIFT = 0
|
||||||
|
EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY3_SHIFT = 4
|
||||||
|
EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY4_SHIFT = 8
|
||||||
|
EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
|
||||||
|
EFUSE_PURPOSE_KEY5_SHIFT = 12
|
||||||
|
|
||||||
|
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
|
||||||
|
EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
|
||||||
|
|
||||||
|
EFUSE_SPI_BOOT_CRYPT_CNT_REG = EFUSE_BASE + 0x034
|
||||||
|
EFUSE_SPI_BOOT_CRYPT_CNT_MASK = 0x7 << 18
|
||||||
|
|
||||||
|
EFUSE_SECURE_BOOT_EN_REG = EFUSE_BASE + 0x038
|
||||||
|
EFUSE_SECURE_BOOT_EN_MASK = 1 << 20
|
||||||
|
|
||||||
|
PURPOSE_VAL_XTS_AES256_KEY_1 = 2
|
||||||
|
PURPOSE_VAL_XTS_AES256_KEY_2 = 3
|
||||||
|
PURPOSE_VAL_XTS_AES128_KEY = 4
|
||||||
|
|
||||||
|
SUPPORTS_ENCRYPTED_FLASH = True
|
||||||
|
|
||||||
|
FLASH_ENCRYPTED_WRITE_ALIGN = 16
|
||||||
|
|
||||||
|
MEMORY_MAP = [
|
||||||
|
[0x00000000, 0x00010000, "PADDING"],
|
||||||
|
[0x40000000, 0x4C000000, "DROM"],
|
||||||
|
[0x4FF00000, 0x4FFA0000, "DRAM"],
|
||||||
|
[0x4FF00000, 0x4FFA0000, "BYTE_ACCESSIBLE"],
|
||||||
|
[0x4FC00000, 0x4FC20000, "DROM_MASK"],
|
||||||
|
[0x4FC00000, 0x4FC20000, "IROM_MASK"],
|
||||||
|
[0x40000000, 0x4C000000, "IROM"],
|
||||||
|
[0x4FF00000, 0x4FFA0000, "IRAM"],
|
||||||
|
[0x50108000, 0x50110000, "RTC_IRAM"],
|
||||||
|
[0x50108000, 0x50110000, "RTC_DRAM"],
|
||||||
|
[0x600FE000, 0x60100000, "MEM_INTERNAL2"],
|
||||||
|
]
|
||||||
|
|
||||||
|
UF2_FAMILY_ID = 0x3D308E94
|
||||||
|
|
||||||
|
EFUSE_MAX_KEY = 5
|
||||||
|
KEY_PURPOSES: Dict[int, str] = {
|
||||||
|
0: "USER/EMPTY",
|
||||||
|
1: "ECDSA_KEY",
|
||||||
|
2: "XTS_AES_256_KEY_1",
|
||||||
|
3: "XTS_AES_256_KEY_2",
|
||||||
|
4: "XTS_AES_128_KEY",
|
||||||
|
5: "HMAC_DOWN_ALL",
|
||||||
|
6: "HMAC_DOWN_JTAG",
|
||||||
|
7: "HMAC_DOWN_DIGITAL_SIGNATURE",
|
||||||
|
8: "HMAC_UP",
|
||||||
|
9: "SECURE_BOOT_DIGEST0",
|
||||||
|
10: "SECURE_BOOT_DIGEST1",
|
||||||
|
11: "SECURE_BOOT_DIGEST2",
|
||||||
|
12: "KM_INIT_KEY",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_pkg_version(self):
|
||||||
|
num_word = 2
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 20) & 0x07
|
||||||
|
|
||||||
|
def get_minor_chip_version(self):
|
||||||
|
num_word = 2
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x0F
|
||||||
|
|
||||||
|
def get_major_chip_version(self):
|
||||||
|
num_word = 2
|
||||||
|
return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 4) & 0x03
|
||||||
|
|
||||||
|
def get_chip_description(self):
|
||||||
|
chip_name = {
|
||||||
|
0: "ESP32-P4",
|
||||||
|
}.get(self.get_pkg_version(), "unknown ESP32-P4")
|
||||||
|
major_rev = self.get_major_chip_version()
|
||||||
|
minor_rev = self.get_minor_chip_version()
|
||||||
|
return f"{chip_name} (revision v{major_rev}.{minor_rev})"
|
||||||
|
|
||||||
|
def get_chip_features(self):
|
||||||
|
return ["High-Performance MCU"]
|
||||||
|
|
||||||
|
def get_crystal_freq(self):
|
||||||
|
# ESP32P4 XTAL is fixed to 40MHz
|
||||||
|
return 40
|
||||||
|
|
||||||
|
def get_flash_voltage(self):
|
||||||
|
pass # not supported on ESP32-P4
|
||||||
|
|
||||||
|
def override_vddsdio(self, new_voltage):
|
||||||
|
raise NotImplementedInROMError(
|
||||||
|
"VDD_SDIO overrides are not supported for ESP32-P4"
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_mac(self, mac_type="BASE_MAC"):
|
||||||
|
"""Read MAC from EFUSE region"""
|
||||||
|
if mac_type != "BASE_MAC":
|
||||||
|
return None
|
||||||
|
mac0 = self.read_reg(self.MAC_EFUSE_REG)
|
||||||
|
mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC
|
||||||
|
bitstring = struct.pack(">II", mac1, mac0)[2:]
|
||||||
|
return tuple(bitstring)
|
||||||
|
|
||||||
|
def get_flash_crypt_config(self):
|
||||||
|
return None # doesn't exist on ESP32-P4
|
||||||
|
|
||||||
|
def get_secure_boot_enabled(self):
|
||||||
|
return (
|
||||||
|
self.read_reg(self.EFUSE_SECURE_BOOT_EN_REG)
|
||||||
|
& self.EFUSE_SECURE_BOOT_EN_MASK
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_key_block_purpose(self, key_block):
|
||||||
|
if key_block < 0 or key_block > self.EFUSE_MAX_KEY:
|
||||||
|
raise FatalError(
|
||||||
|
f"Valid key block numbers must be in range 0-{self.EFUSE_MAX_KEY}"
|
||||||
|
)
|
||||||
|
|
||||||
|
reg, shift = [
|
||||||
|
(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
|
||||||
|
(self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT),
|
||||||
|
][key_block]
|
||||||
|
return (self.read_reg(reg) >> shift) & 0xF
|
||||||
|
|
||||||
|
def is_flash_encryption_key_valid(self):
|
||||||
|
# Need to see either an AES-128 key or two AES-256 keys
|
||||||
|
purposes = [
|
||||||
|
self.get_key_block_purpose(b) for b in range(self.EFUSE_MAX_KEY + 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
if any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return any(p == self.PURPOSE_VAL_XTS_AES256_KEY_1 for p in purposes) and any(
|
||||||
|
p == self.PURPOSE_VAL_XTS_AES256_KEY_2 for p in purposes
|
||||||
|
)
|
||||||
|
|
||||||
|
def change_baud(self, baud):
|
||||||
|
ESPLoader.change_baud(self, baud)
|
||||||
|
|
||||||
|
def _post_connect(self):
|
||||||
|
pass
|
||||||
|
# TODO: Disable watchdogs when USB modes are supported in the stub
|
||||||
|
# if not self.sync_stub_detected: # Don't run if stub is reused
|
||||||
|
# self.disable_watchdogs()
|
||||||
|
|
||||||
|
def check_spi_connection(self, spi_connection):
|
||||||
|
if not set(spi_connection).issubset(set(range(0, 55))):
|
||||||
|
raise FatalError("SPI Pin numbers must be in the range 0-54.")
|
||||||
|
if any([v for v in spi_connection if v in [24, 25]]):
|
||||||
|
print(
|
||||||
|
"WARNING: GPIO pins 24 and 25 are used by USB-Serial/JTAG, "
|
||||||
|
"consider using other pins for SPI flash connection."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ESP32P4StubLoader(ESP32P4ROM):
|
||||||
|
"""Access class for ESP32P4 stub loader, runs on top of ROM.
|
||||||
|
|
||||||
|
(Basically the same as ESP32StubLoader, but different base class.
|
||||||
|
Can possibly be made into a mixin.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c
|
||||||
|
STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM
|
||||||
|
IS_STUB = True
|
||||||
|
|
||||||
|
def __init__(self, rom_loader):
|
||||||
|
self.secure_download_mode = rom_loader.secure_download_mode
|
||||||
|
self._port = rom_loader._port
|
||||||
|
self._trace_enabled = rom_loader._trace_enabled
|
||||||
|
self.cache = rom_loader.cache
|
||||||
|
self.flush_input() # resets _slip_reader
|
||||||
|
|
||||||
|
|
||||||
|
ESP32P4ROM.STUB_CLASS = ESP32P4StubLoader
|
||||||